Skip to content

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.

  1. Parse x-bsv-payment header as JSON
  2. Validate derivationPrefix and derivationSuffix are present and non-empty
  3. Parse AtomicBEEF via Beef.from_binary — extract subject transaction
  4. Derive expected P2PKH script using BRC-42 (KeyDeriver.derive_public_key)
  5. Verify at least one output pays >= required satoshis to derived address
  6. Broadcast the subject transaction to ARC via arc_client.broadcast(subject_tx) (see below)
  7. Consume the derivation prefix (replay protection — atomic, after broadcast succeeds)
  8. Settle via wallet.internalize_action — the wallet records the payment against the derived key
  9. Return x-bsv-payment-result receipt header

Note: BRC105Gateway broadcasts the client's signed BEEF to ARC as the settlement step. The wallet's internalize_action records 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_deriverBSV::Wallet::KeyDeriver instance. Provides the server's identity key and BRC-42 derivation.
  • prefix_store — any object implementing store!, valid?, consume!. The Memory backend is provided for development; use a shared backend for production.
  • wallet — any object implementing #internalize_action(tx:, outputs:, description:). A local WalletClient or RemoteWallet both work.

Process Flow

See process-flow/brc105-gateway.md for sequence diagrams.