x402-rack
Rack middleware for payment-gated HTTP using BSV (Bitcoin SV) and the x402 protocol.
The middleware is a pure dispatcher — it matches routes, issues payment challenges, and routes proofs to pluggable gateway backends for settlement. It has no blockchain knowledge and holds no keys.
What x402-rack guarantees
x402-rack serves content if and only if the vendor has broadcast the payment transaction to the BSV network.
That is the whole job. BRC121Gateway and BRC105Gateway broadcast the client's signed BEEF to ARC themselves, after the payment output has been verified but before any wallet or replay state is mutated. The broadcast is idempotent — a client that already broadcast is fine; ARC treats the duplicate as a no-op. This matches BSV's native commerce model: the vendor is the settlement point.
ProofGateway keeps its original semantics (client broadcasts first, server reads ARC status) because the scheme is explicitly proof-of-prior-payment. Every other gateway broadcasts.
Two failure modes are distinguished in the HTTP response:
402 Payment Required— ARC rejected the broadcast (malformed tx, insufficient funds, double-spend). This is a client fault.503 Service Unavailable— ARC is unreachable, timing out, or returning 5xx. This is an infrastructure fault; the payment may be legitimate but cannot be settled.
ARC is a hard runtime dependency in the critical path. Operators should monitor ARC reachability and latency the same way they monitor their own database. If ARC is down, x402-rack cannot settle payments — no broadcast, no content. There is no kill-switch: vendor-broadcast is not optional, because without it there is no meaningful enforcement. For dev/staging flows, mock the gateway or avoid enabling payment middleware on un-gated routes.
Mental model: x402-rack is the checkout
Customer → x402-rack (checkout) → Vendor (the app)
├─ verify tx pays right
├─ broadcast to ARC ← cash register ding
├─ record in wallet ← till drawer closes
└─ return 200 → vendor serves content
x402-rack is the point-of-sale terminal sitting between the customer and the merchant's backend. The customer presents a signed transaction, the checkout settles it on-chain, and only then does the vendor's app deliver the goods. Same role a card reader plays at a physical till.
This framing explains the design directly: vendor-broadcast is "the card reader dials the bank", 503 is "TERMINAL DOWN", 402 is "CARD DECLINED", no kill-switch means a till that can't refuse to process payments isn't a till. See docs/architecture.md for the full mapping.
Installation
# Gemfile
gem "x402-rack"
bundle install
Quick start
Minimal: relay to an existing wallet
The simplest setup — no keys, no local wallet, no ARC configuration. Point operator_wallet_url at an existing @bsv/simple server wallet and PayGateway is auto-enabled:
X402.configure do |c|
c.domain = "api.example.com"
c.operator_wallet_url = "https://my-wallet.example.com/api/server-wallet"
c.protect method: :GET, path: "/api/data", amount_sats: 100
end
use X402::Middleware
ARC defaults to ARCADE. The server holds no private keys — it derives unique payment addresses from the remote wallet's public key, broadcasts via ARC, then relays the settlement to the wallet for UTXO tracking.
With a local wallet (enables BRC-121)
Set up a server wallet:
bundle exec rake x402:wallet:setup
Rails apps get the task automatically via the Railtie. Non-Rails Rack apps need one line in their
Rakefile:load "x402/tasks/x402.rake"
Then add the middleware:
X402.configure do |c|
c.domain = "api.example.com"
c.wallet = X402::Wallet.load
c.protect method: :GET, path: "/api/data", amount_sats: 100
end
use X402::Middleware
Both PayGateway and BRC121Gateway are auto-enabled. ARC defaults to ARCADE.
With a remote wallet for all gateways
RemoteWallet implements the same duck-typed interface as the local wallet, so all gateways work with it — PayGateway, BRC-121, and BRC-105:
X402.configure do |c|
c.domain = "api.example.com"
c.wallet = X402::RemoteWallet.new(url: "https://my-wallet.example.com/api/server-wallet")
c.protect method: :GET, path: "/api/data", amount_sats: 100
end
use X402::Middleware
With wallet: set and no explicit enable calls, gateways are auto-wired:
PayGateway— works with or without a wallet (the only gateway that does). Without a wallet, it usesoperator_wallet_urlfor keyless relay. With a wallet, it usesinternalize_actionfor settlement — converging with BRC-121's approach.BRC121Gateway— always enabled whenwallet:is set. Requires a wallet (local or remote).BRC105Gateway— opt-in viaconfig.enable :brc105_gateway. Requires a wallet.
Clients can pay using whichever protocol they support; the middleware dispatches on proof header.
Gateways
| Gateway | Wallet required? | Setup required | Status |
|---|---|---|---|
PayGateway (Coinbase v2) |
No — works with operator_wallet_url alone |
Auto-enabled | Stable |
BRC121Gateway (BRC-121) |
Yes (local or remote) | Auto-enabled when wallet: set |
Stable |
BRC105Gateway (BRC-105) |
Yes (local or remote) | config.enable :brc105_gateway |
Transitional |
ProofGateway (merkleworks) |
No | config.enable :proof_gateway |
Experimental |
PayGateway is the only gateway that works without a wallet — it derives payment addresses from the operator's public key and relays settlement after broadcast. BRC-121 and BRC-105 require a wallet for internalize_action. ARC defaults to ARCADE when no explicit arc_url is configured.
Advanced configuration
Explicit gateway enablement
X402.configure do |config|
config.domain = "api.example.com"
config.wallet = X402::Wallet.load
config.enable :pay_gateway # explicit, same as default
config.enable :brc105_gateway # opt in to BRC-105
config.enable :proof_gateway, nonce_provider: my_np # opt in to ProofGateway
end
If any config.enable calls are made, the auto-enable is skipped — you get exactly what you asked for.
Per-gateway overrides
config.enable :pay_gateway, arc_client: my_custom_arc # override ARC broadcaster
config.enable :brc121_gateway, wallet: alt_wallet # override wallet
Manual gateway construction (power-user escape hatch)
X402.configure do |config|
config.domain = "api.example.com"
config.gateways = [
X402::BSV::PayGateway.new(
arc_client: BSV::Network::ARC.default,
wallet: my_wallet
),
X402::BSV::BRC121Gateway.new(wallet: my_wallet)
]
config.protect method: :GET, path: "/api/expensive", amount_sats: 100
end
When config.gateways is set, any enable calls and the auto-enable are ignored.
Wallet options
X402::Wallet.load resolves the signing key in this order:
SERVER_WIFenvironment variable (wins if set)~/.bsv-wallet/wallet.key(orBSV_WALLET_DIR/wallet.key) — written byrake x402:wallet:setup- Raises
ConfigurationErrorsuggesting the setup task
The Rake task never overwrites an existing wallet.key. Pass FORCE=1 to replace an existing wallet (destructive).
Backwards-compat alternatives still work: config.server_wif = ENV["SERVER_WIF"] or config.payee_locking_script_hex = "76a914...88ac".
How It Works
- Client requests a protected resource
- Middleware returns
402 Payment Requiredwith challenge headers from each configured gateway - Client constructs a BSV payment transaction and retries with proof
- Middleware dispatches the proof to the matching gateway for settlement
- Gateway verifies and settles — middleware serves or rejects
Four BSV settlement schemes are supported:
- BSV-pay (Coinbase v2 headers) — server broadcasts via ARC (defaults to ARCADE). Partial transaction template, unique derived addresses per payment. Works without a wallet via
operator_wallet_url. - BRC-121 (BSV Association simple) — stateless, BRC-100 wallet-native, zero config.
- BRC-105 (BSV Association authenticated) — settlement via
wallet.internalize_action. Transitional; requires BRC-103 for spec compliance. - BSV-proof (merkleworks) — experimental; client broadcasts, server checks mempool.
See CHANGELOG.md for release history and docs/ for full documentation.
Development
bin/setup # Install dependencies
bundle exec rake spec # Run unit and integration tests
bundle exec rubocop # Lint
bundle exec rake # Run all checks (tests + lint)
bundle exec rake e2e # Run BSV testnet e2e tests (requires ARC + funded wallets)
bundle exec rake feature # Run browser feature tests (requires Chrome for Testing + bsv-x402 extension)
Feature specs (browser)
Feature specs drive a real Chrome browser with the bsv-x402 extension side-loaded. Google Chrome stable silently refuses --load-extension, so a separate automation build is required:
npx @puppeteer/browsers install chrome@stable \
--path=$HOME/.cache/chrome-for-testing
npx @puppeteer/browsers install chromedriver@stable \
--path=$HOME/.cache/chrome-for-testing
bundle install --with feature
bundle exec rake feature
Set BSV_X402_EXTENSION_PATH to override the default extension location. Set HEADED=1 to launch Chrome in headed mode for debugging.
Contributing
Bug reports and pull requests are welcome on GitHub.
Licence
Available under the terms of the Open BSV Licence.