Architecture¶
Core invariant¶
x402-rack serves content if and only if the payment transaction is visible on the BSV network.
This is the one job. Every gateway enforces it at the same point in the settle flow: after the BEEF has been parsed and the payment output verified, but before any wallet or replay state is mutated. The check lives in X402::BSV::NetworkVisibility.verify! and is shared by PayGateway, ProofGateway, BRC121Gateway, and BRC105Gateway.
Failures bifurcate cleanly in the HTTP response:
- 402 — transaction not visible after bounded retries (client fault)
- 503 — ARC unreachable / 5xx / timeout (infrastructure fault)
See schemes/brc-121.md and schemes/brc-105.md for the per-gateway specifics.
Middleware as Dispatcher (X402::Middleware)¶
The Rack middleware is a pure dispatcher — the gatekeeper. It has no blockchain knowledge. It:
- Matches incoming requests against protected routes
- Polls each configured gateway for challenge headers, returns all of them in the 402 response
- Checks which proof/payment header the client sent, dispatches to the matching gateway
- The gateway returns allow/deny — the middleware serves or rejects accordingly
The middleware never decodes transactions, checks mempool, broadcasts, or interacts with any blockchain network. It manages HTTP headers, route matching, and dispatch.
The gatekeeper MUST NOT sign transactions or hold private keys.
Gateways¶
Gateways are pluggable backends that handle chain-specific settlement. They delegate key management and signing to external providers (e.g. the treasury via nonce_provider). Each gateway:
- Builds challenge data (including partial transaction templates)
- Verifies and settles proofs
- Verifies on-chain visibility via ARC before mutating wallet state (the NO PAY → NO CONTENT invariant)
- Interacts with ARC and/or a treasury service via the BSV wallet
Gateway Interface¶
# #challenge_headers(rack_request, route) → Hash
# #proof_header_names → Array<String>
# #settle!(header_name, proof_payload, rack_request, route) → result
The boundary test: could someone write X402::EVM::Gateway implementing this interface without touching lib/x402/? If yes, the separation is correct.
Built-in Gateways¶
X402::BSV::PayGateway— Coinbase v2 headers, server broadcasts via ARC. See schemes/bsv-pay.md.X402::BSV::BRC121Gateway— BRC-121 simple payments, stateless, BRC-100 wallet-native. See schemes/brc-121.md.X402::BSV::BRC105Gateway— BRC-105 authenticated payments, BRC-29 derived addresses, AtomicBEEF transactions. See schemes/brc-105.md.X402::BSV::ProofGateway— merkleworks headers, client broadcasts, server checks mempool. See schemes/bsv-proof.md.
Payment Content Negotiation¶
Different x402 ecosystems use different HTTP headers. A server can send multiple challenge headers simultaneously — the client picks the one it can satisfy.
| Scheme | Challenge headers | Proof header | Receipt header |
|---|---|---|---|
| BSV-pay (ours) | Payment-Required | Payment-Signature | Payment-Response |
| BRC-121 (BSV Association) | x-bsv-payment-satoshis-required, x-bsv-payment-version | x-bsv-beef | x-bsv-payment-satoshis-paid |
| BRC-105 (BSV Association) | x-bsv-payment-satoshis-required, x-bsv-payment-derivation-prefix, x-bsv-payment-identity-key* | x-bsv-payment | x-bsv-payment-result |
| BSV-proof (merkleworks) | X402-Challenge | X402-Proof | — |
* x-bsv-payment-identity-key is omitted when BRC-103 middleware is present upstream.
Header namespaces are reserved per ecosystem: - Payment-* — Coinbase v2 / our PayGateway - x-bsv-* — BRC-121 and BRC-105 / BSV Association (our BRC121Gateway and BRC105Gateway) - X402-* — merkleworks / our ProofGateway
Transaction Models¶
Template-based (PayGateway, ProofGateway)¶
Both template-based gateways produce partial transaction templates that the client extends by adding funding inputs (and optionally change outputs).
Base behaviour (X402::BSV::Gateway): build a partial tx with the payment output (amount to payee) and an OP_RETURN request binding output.
ProofGateway override: prepends the nonce UTXO input at index 0, signed with SIGHASH_SINGLE | ANYONECANPAY | FORKID (0xC3). This locks the payment output (output 0) while allowing the client to append inputs and outputs freely.
PayGateway: inherits the base behaviour. Payment output + OP_RETURN binding, no nonce.
The client's job is identical for template-based gateways: add funding inputs, sign, and either broadcast (BSV-proof) or hand to the server (BSV-pay).
Derivation-based (BRC105Gateway)¶
BRC105Gateway uses a fundamentally different approach — no partial transaction template. Instead:
- The server advertises a derivation prefix (random nonce) and its identity key
- The client derives a unique payment address using BRC-29 (BRC-42 key derivation with protocol ID
[2, "3241645161d8"]and key ID"#{prefix} #{suffix}") - The client builds the entire transaction independently, paying to the derived address
- The server re-derives the expected address and verifies the payment output
This eliminates the need for OP_RETURN binding, payTo HMAC, or any shared transaction state. BRC105Gateway does not inherit from Gateway — it uses composition via KeyDeriver.
Request Binding via OP_RETURN¶
The x402 protocol tag makes payments discoverable on-chain. The SHA-256 hash binds the payment to the specific HTTP request. Configurable strict/permissive mode.
Why 0xC3 for the Nonce Signature¶
SIGHASH_SINGLE | ANYONECANPAY | FORKID: - SIGHASH_SINGLE: commits only to output[input_index] — the nonce at input 0 protects only output 0 (the payment) - ANYONECANPAY: excludes other inputs — funding and fee inputs can be appended freely - FORKID: BSV fork ID flag (required)
Using 0xC1 (SIGHASH_ALL | ANYONECANPAY) would commit to ALL outputs, breaking extensibility.
Component Boundaries¶
| Component | Responsibility | Keys? |
|---|---|---|
Gatekeeper (X402::Middleware) | HTTP dispatch, route matching | No — MUST NOT hold keys |
Gateway (X402::BSV::*Gateway) | Challenge templates, settlement, ARC interaction | Via wallet |
BSV Wallet (bsv-wallet gem) | Key management, UTXO tracking, signing | Yes — the security boundary |
| Treasury (wallet role) | Mints nonce UTXOs, signs templates | Via wallet's nonce basket |
| Delegator (separate service) | Adds fee inputs, signs only fee inputs | Yes — but not our concern |
| Client (browser + CWI wallet) | Extends template, signs funding inputs | Yes — client's wallet |
Dependency Chain¶
x402-rack (no keys, no wallet dependency in middleware)
└── X402::BSV::*Gateway → bsv-wallet (BRC-100 interface)
└── bsv-sdk (primitives)
The middleware itself has no dependency on bsv-wallet or bsv-sdk. Only the gateway classes do.