x402-rack

Rack middleware for payment-gated HTTP using BSV (Bitcoin SV) and the x402 protocol.

The middleware is a pure dispatcher — it matches routes, issues payment challenges, and routes proofs to pluggable gateway backends for settlement. It has no blockchain knowledge and holds no keys.

Installation

# Gemfile
gem "x402-rack"
bundle install

Quick start

Minimal: relay to an existing wallet

The simplest setup — no keys, no local wallet, no ARC configuration. Point operator_wallet_url at an existing @bsv/simple server wallet and PayGateway is auto-enabled:

X402.configure do |c|
  c.domain = "api.example.com"
  c.operator_wallet_url = "https://my-wallet.example.com/api/server-wallet"
  c.protect method: :GET, path: "/api/data", amount_sats: 100
end

use X402::Middleware

ARC defaults to ARCADE. The server holds no private keys — it derives unique payment addresses from the remote wallet's public key, broadcasts via ARC, then relays the settlement to the wallet for UTXO tracking.

With a local wallet (enables BRC-121)

Set up a server wallet:

bundle exec rake x402:wallet:setup

Rails apps get the task automatically via the Railtie. Non-Rails Rack apps need one line in their Rakefile:

load "x402/tasks/x402.rake"

Then add the middleware:

X402.configure do |c|
  c.domain = "api.example.com"
  c.wallet = X402::Wallet.load
  c.protect method: :GET, path: "/api/data", amount_sats: 100
end

use X402::Middleware

Both PayGateway and BRC121Gateway are auto-enabled. ARC defaults to ARCADE.

With a remote wallet for all gateways

RemoteWallet implements the same duck-typed interface as the local wallet, so all gateways work with it — PayGateway, BRC-121, and BRC-105:

X402.configure do |c|
  c.domain = "api.example.com"
  c.wallet = X402::RemoteWallet.new(url: "https://my-wallet.example.com/api/server-wallet")
  c.protect method: :GET, path: "/api/data", amount_sats: 100
end

use X402::Middleware

With wallet: set and no explicit enable calls, gateways are auto-wired:

  • PayGateway — works with or without a wallet (the only gateway that does). Without a wallet, it uses operator_wallet_url for keyless relay. With a wallet, it uses internalize_action for settlement — converging with BRC-121's approach.
  • BRC121Gateway — always enabled when wallet: is set. Requires a wallet (local or remote).
  • BRC105Gateway — opt-in via config.enable :brc105_gateway. Requires a wallet.

Clients can pay using whichever protocol they support; the middleware dispatches on proof header.

Gateways

Gateway Wallet required? Setup required Status
PayGateway (Coinbase v2) No — works with operator_wallet_url alone Auto-enabled Stable
BRC121Gateway (BRC-121) Yes (local or remote) Auto-enabled when wallet: set Stable
BRC105Gateway (BRC-105) Yes (local or remote) config.enable :brc105_gateway Transitional
ProofGateway (merkleworks) No config.enable :proof_gateway Experimental

PayGateway is the only gateway that works without a wallet — it derives payment addresses from the operator's public key and relays settlement after broadcast. BRC-121 and BRC-105 require a wallet for internalize_action. ARC defaults to ARCADE when no explicit arc_url is configured.

Advanced configuration

Explicit gateway enablement

X402.configure do |config|
  config.domain = "api.example.com"
  config.wallet = X402::Wallet.load

  config.enable :pay_gateway                           # explicit, same as default
  config.enable :brc105_gateway                        # opt in to BRC-105
  config.enable :proof_gateway, nonce_provider: my_np  # opt in to ProofGateway
end

If any config.enable calls are made, the auto-enable is skipped — you get exactly what you asked for.

Per-gateway overrides

config.enable :pay_gateway, arc_client: my_custom_arc  # override ARC broadcaster
config.enable :brc121_gateway, wallet: alt_wallet      # override wallet

Manual gateway construction (power-user escape hatch)

X402.configure do |config|
  config.domain = "api.example.com"
  config.gateways = [
    X402::BSV::PayGateway.new(
      arc_client: BSV::Network::ARC.default,
      wallet: my_wallet
    ),
    X402::BSV::BRC121Gateway.new(wallet: my_wallet)
  ]
  config.protect method: :GET, path: "/api/expensive", amount_sats: 100
end

When config.gateways is set, any enable calls and the auto-enable are ignored.

Wallet options

X402::Wallet.load resolves the signing key in this order:

  1. SERVER_WIF environment variable (wins if set)
  2. ~/.bsv-wallet/wallet.key (or BSV_WALLET_DIR/wallet.key) — written by rake x402:wallet:setup
  3. Raises ConfigurationError suggesting the setup task

The Rake task never overwrites an existing wallet.key. Pass FORCE=1 to replace an existing wallet (destructive).

Backwards-compat alternatives still work: config.server_wif = ENV["SERVER_WIF"] or config.payee_locking_script_hex = "76a914...88ac".

How It Works

  1. Client requests a protected resource
  2. Middleware returns 402 Payment Required with challenge headers from each configured gateway
  3. Client constructs a BSV payment transaction and retries with proof
  4. Middleware dispatches the proof to the matching gateway for settlement
  5. Gateway verifies and settles — middleware serves or rejects

Four BSV settlement schemes are supported:

  • BSV-pay (Coinbase v2 headers) — server broadcasts via ARC (defaults to ARCADE). Partial transaction template, unique derived addresses per payment. Works without a wallet via operator_wallet_url.
  • BRC-121 (BSV Association simple) — stateless, BRC-100 wallet-native, zero config.
  • BRC-105 (BSV Association authenticated) — settlement via wallet.internalize_action. Transitional; requires BRC-103 for spec compliance.
  • BSV-proof (merkleworks) — experimental; client broadcasts, server checks mempool.

See CHANGELOG.md for release history and docs/ for full documentation.

Development

bin/setup               # Install dependencies
bundle exec rake spec   # Run unit and integration tests
bundle exec rubocop     # Lint
bundle exec rake        # Run all checks (tests + lint)
bundle exec rake e2e    # Run BSV testnet e2e tests (requires ARC + funded wallets)
bundle exec rake feature # Run browser feature tests (requires Chrome for Testing + bsv-x402 extension)

Feature specs (browser)

Feature specs drive a real Chrome browser with the bsv-x402 extension side-loaded. Google Chrome stable silently refuses --load-extension, so a separate automation build is required:

npx @puppeteer/browsers install chrome@stable \
  --path=$HOME/.cache/chrome-for-testing
npx @puppeteer/browsers install chromedriver@stable \
  --path=$HOME/.cache/chrome-for-testing
bundle install --with feature
bundle exec rake feature

Set BSV_X402_EXTENSION_PATH to override the default extension location. Set HEADED=1 to launch Chrome in headed mode for debugging.

Contributing

Bug reports and pull requests are welcome on GitHub.

Licence

Available under the terms of the Open BSV Licence.