Skip to content

Class X402::BSV::BRC121Gateway

Inherits: Object

BRC-121 ("Simple 402 Payments") gateway for BSV settlement-gated HTTP.

BRC-121 is the BSV Association's simple HTTP payment protocol. The server is stateless: replay protection comes from a 30-second timestamp freshness window (§5 step 2) plus the wallet's handling of duplicate incoming transactions.

Unlike BRC-105, BRC-121 does not use server-generated derivation prefixes or a PrefixStore. The client generates the derivation prefix, chooses a timestamp as the suffix, constructs a BRC-29 payment, and submits it all in one round trip with no prior handshake.

Spec: https://hub.bsvblockchain.org/brc/payments/0121

Headers

Challenge (server → client): x-bsv-sats — required satoshi amount x-bsv-server — server's identity public key (compressed hex)

Proof (client → server): x-bsv-beef — base64 BEEF transaction (the proof header) x-bsv-sender — client's identity public key (compressed hex) x-bsv-nonce — base64 BRC-29 derivation prefix x-bsv-time — decimal Unix millisecond timestamp x-bsv-vout — decimal output index of the payment

Replay protection

BRC-121 §5 step 2: the x-bsv-time header MUST be within 30 seconds of the server's current time. This is the primary defence against capture and delayed replay.

BRC-121 §5 step 5 also specifies checking isMerge on the wallet's internalization result. The current Ruby BSV::Wallet::WalletClient does not return an isMerge field, so this gateway additionally uses an X402::BSV::TxidStore to reject duplicate txids within the freshness window.

Implements the three-method gateway interface required by Middleware: challenge_headers(rack_request, route) → Hash proof_header_names → Array settle!(header_name, proof_payload, rack_request, route) → SettlementResult

Constants

BASE64_NONCE

BRC-29 derivation prefix is base64. Cap length at 128 chars (~96 bytes decoded) — well above realistic nonce sizes.

CLIENT_HEADERS

Not documented.

COMPRESSED_PUBKEY_HEX

Not documented.

FRESHNESS_WINDOW_MS

Not documented.

PROOF_HEADER

Not documented.

PROTOCOL

Not documented.

Public Instance Methods

challenge_headers(_rack_request, route)

Issue a 402 challenge with BRC-121 headers. - @param _rack_request [Rack::Request] unused (stateless protocol) - @param route [X402::Configuration::Route] - @return [Hash] challenge headers (+x-bsv-sats+, +x-bsv-server+)

initialize(wallet:, txid_store: = nil, arc_client: = nil, network_visibility_cache: = nil)

  • @param wallet [#internalize_action, #get_public_key] BRC-100 wallet. Must respond to +#internalize_action(args)+ (per bsv-ruby-sdk +BSV::Wallet::WalletClient+) and +#get_public_key(identity_key: true)+ for the server identity key.
  • @param txid_store [#record_if_unseen!, nil] replay protection for settled txids. Defaults to +X402::BSV::TxidStore::Memory.new+.
  • @param arc_client [#status, nil] ARC client used to confirm the payment txid is visible on the BSV network before the wallet internalises it. When +nil+ (backward compatibility), the visibility check is skipped entirely.
  • @param network_visibility_cache [NetworkVisibility::Cache, nil] per-gateway positive-only TTL cache shielding ARC from duplicate-submission bursts. Defaults to a fresh +NetworkVisibility::Cache.new+.
  • @return [BRC121Gateway] a new instance of BRC121Gateway

proof_header_names()

Header names that carry the proof/payment from the client. - @return [Array]

settle!(_header_name, _proof_payload, rack_request, route)

Verify and internalise a BRC-121 payment. - @param _header_name [String] which proof header matched - @param _proof_payload [String] the +x-bsv-beef+ header value (read directly from the rack env alongside the other four client headers) - @param rack_request [Rack::Request] - @param route [X402::Configuration::Route] - @raise [VerificationError] on missing/invalid headers, stale timestamp, insufficient payment, replay, or internalisation failure - @return [SettlementResult]