BRC-105 Scheme (BSV Association)¶
The official BSV Association BRC-105 HTTP Service Monetization Framework.
Transitional — requires BRC-103 middleware for spec compliance
BRC-105 requires BRC-103 mutual authentication per §3, §5.1, and §7.1 of the spec. The canonical text is unambiguous: "If not authenticated, respond 401 Unauthorized." There is no standalone mode.
This implementation accepts the client identity key via x-bsv-auth-identity-key HTTP header as a transitional measure until proper BRC-103 middleware is available in Ruby. Clients that send the auth header (e.g. bsv-x402) work today, but this deviates from the spec.
PayGateway and BRC121Gateway work zero-config, require no custom client instrumentation, and — for BRC-121 — are themselves spec-compliant.
Description¶
BRC-29 derived payment addresses with AtomicBEEF transactions. BSV Association x-bsv-* headers. Requires BRC-103 mutual authentication.
Unlike BSV-pay and BSV-proof, this scheme does not use partial transaction templates. The client builds the entire transaction using a derived payment address.
Headers¶
| Direction | Header | Content |
|---|---|---|
| Server -> Client | x-bsv-payment-satoshis-required | Amount in satoshis |
| Server -> Client | x-bsv-payment-derivation-prefix | Random BRC-29 nonce |
| Server -> Client | x-bsv-payment-identity-key* | Server's compressed public key (hex) |
| Client -> Server | x-bsv-payment | JSON with derivation + AtomicBEEF |
| Server -> Client | x-bsv-payment-result* | base64(settlement result JSON) |
* x-bsv-payment-identity-key is omitted when BRC-103 middleware is present — the client already has the server's key from the handshake. * x-bsv-payment-result is a non-standard extension (BRC-105 defines no receipt header). Included for consistency with PayGateway's Payment-Response.
Challenge¶
The 402 response carries individual headers (not a JSON blob):
HTTP/1.1 402 Payment Required
x-bsv-payment-satoshis-required: 100
x-bsv-payment-derivation-prefix: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
x-bsv-payment-identity-key: 02ab...cd
Payment¶
The client: 1. Chooses a random derivation suffix 2. Derives the payment address via BRC-29: - Protocol ID: [2, "3241645161d8"] - Key ID: "#{prefix} #{suffix}" - Counterparty: server's identity key (from header or BRC-103 session) 3. Builds a transaction paying to the derived P2PKH address 4. Encodes as AtomicBEEF (BRC-95), base64
x-bsv-payment: {"derivationPrefix":"a1b2...","derivationSuffix":"f7e8...","transaction":"<base64 AtomicBEEF>"}
Settlement Flow¶
The gateway enforces a verify then broadcast then internalise ordering: cheap validation first, then the gateway broadcasts the client's signed BEEF to ARC itself, then prefix consumption, then wallet internalisation. Broadcasting before consuming the prefix is deliberate — it lets a legitimate retry after a transient ARC failure reuse the same prefix, which consumption-first would forbid.
- Parse
x-bsv-paymentheader as JSON - Validate
derivationPrefixandderivationSuffixare present and non-empty - Parse AtomicBEEF via
Beef.from_binary— extract subject transaction - Derive expected P2PKH script using BRC-42 (
KeyDeriver.derive_public_key) - Verify at least one output pays >= required satoshis to derived address
- Broadcast the subject transaction to ARC via
arc_client.broadcast(subject_tx)(see below) - Consume the derivation prefix (replay protection — atomic, after broadcast succeeds)
- Settle via
wallet.internalize_action— the wallet records the payment against the derived key - Return
x-bsv-payment-resultreceipt header
Note: BRC105Gateway broadcasts the client's signed BEEF to ARC as the settlement step. The wallet's
internalize_actionrecords the payment against the derived key — it does not itself broadcast. This matches BSV's commerce model where the vendor is the settlement point.
Vendor broadcast¶
Step 6 is the load-bearing check. A structurally valid AtomicBEEF proves nothing about whether the client actually broadcast the transaction — and under BSV's commerce model, it doesn't have to. The vendor is the settlement point. The gateway calls arc_client.broadcast(subject_tx) itself, classifying the outcome into three buckets:
| ARC response | Interpretation | HTTP response |
|---|---|---|
| 2xx (accepted, or already-seen idempotency) | Tx is on the network | Continue to consume + internalise → 200 |
| 4xx (malformed, insufficient funds, double-spend) | Client submitted an invalid payment | 402 "payment broadcast rejected" |
| 5xx / unreachable / timeout | Infrastructure fault, not client fault | 503 "payment verification temporarily unavailable" |
The broadcast-before-consume ordering matters for retry semantics. If broadcast were placed after prefix consumption, a client who hit a transient ARC outage would see their 503 response but have no way to retry: the prefix is gone. By consuming only after broadcast succeeds, a genuine client can retry with the same prefix within the challenge TTL — the failed attempt leaves no trace.
ARC is idempotent — if the client already broadcast the same tx, ARC returns 2xx rather than duplicating work. Self-funded clients and no_send clients are handled by the same code path.
ARC is a hard dependency
Vendor-broadcast makes ARC reachability a hard runtime dependency. Operators should monitor ARC health (5xx rate, latency) the same way they monitor their database. An ARC outage will surface as elevated 503s. Elevated 402s instead indicate client misbehaviour (malformed AtomicBEEF, insufficient funds, double-spend attempts) — the distinction is deliberate and should drive different alert paths.
There is no kill-switch. Vendor-broadcast is not optional — without it there is no meaningful enforcement of NO PAY → NO CONTENT. For dev or staging flows that don't broadcast, mock the gateway or avoid enabling payment middleware on un-gated routes.
Replay Protection¶
Server-tracked derivation prefixes. Each challenge generates a unique prefix (128 bits of randomness). The prefix is consumed atomically at settlement after full transaction validation. Double-consumption is rejected.
The in-memory prefix store (PrefixStore::Memory) enforces: - TTL: prefixes expire after 300 seconds (configurable) - Max capacity: 10,000 unconsumed prefixes (configurable)
Production multi-process deployments should use a shared backend (Redis, database).
Key Derivation (BRC-29)¶
BRC-29 uses BRC-42 key derivation with ECDH:
invoice_number = "2-3241645161d8-#{prefix} #{suffix}"
derived_pubkey = ECDH_derive(server_identity_key, invoice_number)
payment_address = P2PKH(derived_pubkey)
Each payment goes to a unique derived address — no address reuse, no payTo HMAC needed. The server can re-derive the private key to spend the output.
BRC-103 Composition¶
| Aspect | Standalone | With BRC-103 |
|---|---|---|
| Server identity key | Advertised in x-bsv-payment-identity-key header | From BRC-103 handshake |
| BRC-29 counterparty | "anyone" | Client's authenticated identity key |
| Client identity | Unknown | env['brc103.identity_key'] |
| Replay protection | Prefix tracking only | Prefix + session nonces |
The gateway detects BRC-103 presence automatically via env['brc103.identity_key']. A valid compressed public key triggers authenticated mode; anything else falls back to standalone.
Differences from BSV-pay and BSV-proof¶
| Feature | BSV-pay | BSV-proof | BRC-105 |
|---|---|---|---|
| Transaction model | Partial template | Pre-signed template | Client-built (no template) |
| Payment binding | OP_RETURN hash | OP_RETURN + nonce UTXO | BRC-29 derivation |
| Replay protection | ARC (chain-native) | Nonce UTXO (single-spend) | Derivation prefix (server state) |
| Transaction format | Raw bytes | Raw bytes | AtomicBEEF (BRC-95) |
| Identity | None | None | Optional (BRC-103) |
| Base class | Gateway | Gateway | None (composition via KeyDeriver) |
| Infrastructure | ARC | ARC + Treasury | Wallet + Prefix Store |
Configuration¶
X402::BSV::BRC105Gateway.new(
key_deriver: BSV::Wallet::KeyDeriver.new(server_private_key),
prefix_store: X402::BSV::PrefixStore::Memory.new(ttl: 300, max_issued: 10_000),
wallet: wallet
)
key_deriver—BSV::Wallet::KeyDeriverinstance. Provides the server's identity key and BRC-42 derivation.prefix_store— any object implementingstore!,valid?,consume!. TheMemorybackend is provided for development; use a shared backend for production.wallet— any object implementing#internalize_action(tx:, outputs:, description:). A localWalletClientorRemoteWalletboth work.
Process Flow¶
See process-flow/brc105-gateway.md for sequence diagrams.