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 internalise ordering: cheap validation first, then on-chain visibility via ARC, then prefix consumption, then wallet internalisation. Verifying before consuming the prefix is deliberate — it lets a legitimate retry after a re-broadcast 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. Verify on-chain visibility via arc_client.status(txid) (see below)
  7. Consume the derivation prefix (replay protection — atomic, after verify passes)
  8. Settle via wallet.internalize_action — the wallet handles broadcast internally
  9. Return x-bsv-payment-result receipt header

Note: BRC105Gateway no longer broadcasts directly to ARC. Settlement is delegated to the wallet's internalize_action, which validates the transaction and manages broadcast. This aligns with the BRC-105 spec's mandate for wallet-based settlement and converges with BRC121Gateway's approach.

Verify on-chain

Step 6 is the load-bearing check. A structurally valid AtomicBEEF proves nothing about whether the client actually broadcast the transaction — only ARC observation does. The gateway polls arc_client.status(txid) with bounded retries (4 attempts, ~1.75 s worst case) to absorb propagation lag, classifying the outcome into three buckets:

ARC status Interpretation HTTP response
SEEN_ON_NETWORK, MINED Tx is live on the network Continue to consume + internalise → 200
UNKNOWN / not found (after retries) Client never broadcast — no_send exploit, client bug, or wrong ARC 402 "payment transaction not visible on the BSV network"
ARC unreachable / 5xx / timeout (after retries) Infrastructure fault, not client fault 503 "payment verification temporarily unavailable"

The verify-before-consume ordering matters for retry semantics. If verification were placed after prefix consumption, a client whose first broadcast failed to propagate would see their 402 response but have no way to retry: the prefix is gone. By consuming only after verification passes, a genuine client can re-broadcast and retry with the same prefix within the challenge TTL — the failed attempt leaves no trace.

Positive observations are cached per-gateway for ~30 s keyed by txid, protecting ARC from duplicate-submission bursts. Negative observations are never cached — propagation can flip UNKNOWNSEEN_ON_NETWORK at any moment, and a cached negative would delay legitimate payments.

ARC is a hard dependency

On-chain verification 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 or exploit attempts — the distinction is deliberate and should drive different alert paths.

For local development and unit testing where wallets may never broadcast, set config.verify_on_chain = false (or X402_VERIFY_ON_CHAIN=false). The default is true; disabling it emits a WARN banner at startup.

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.