close
Skip to content

Repository files navigation

bx402

CI made-with-typescript

A pay-per-request proxy in front of the Brave Search API. Instead of an API key, each request carries a stablecoin micropayment, making the signed payment act as the credential.

bx402 is bx (Brave Search CLI) + 402, the HTTP Payment Required status. The 402 is the shared mechanism, not a rail, so x402 (USDC on Base) and MPP (pathUSD on Tempo) are equally first-class.

Spec https://gist.github.com/onyb/a1d620ba1e6ded2577a2998f2ecb0f61

How it works

  1. A request with no payment receives one 402 Payment Required that advertises both rails.
  2. The client retries with the header matching its wallet, either x402 or MPP.
  3. On a valid payment the request is forwarded to Brave Search and the result is returned with the settlement receipt.

One hostname serves both rails. The rail is chosen by the client's payment header.

Prerequisites

You need Node.js 24 or newer with corepack enable (pnpm comes from the packageManager field), and a Brave Search API key (free tier at brave.com/search/api).

Paying for a search on Base Sepolia

This runs bx402 in Docker against the public x402.org facilitator, which verifies each payment and settles it on chain. It serves testnet only and pays the settlement gas itself, so nothing on the Brave side needs funding. Production points X402_FACILITATOR_URL at the Coinbase-hosted facilitator instead, with CDP_API_KEY_ID and CDP_API_KEY_SECRET set beside it; the service signs every facilitator call with that key and refuses to send credentials to any other facilitator host.

Party Owner Requires Faucet
Payer wallet Agent Base Sepolia USDC Circle
Treasury address Brave nothing
  1. Clone, then write your Brave Search API key to .env:
    git clone git@github.com:brave-experiments/bx402.git
    cd bx402
    echo "BRAVE_SEARCH_API_KEY=<your-key>" >> .env
  2. Start the stack:
    docker compose up --build -d
  3. Create a payer wallet and fund it with USDC on Base Sepolia (brew install stripe/purl/purl if needed):
    purl wallet add --type evm
    purl balance --network base-sepolia
  4. Pay for a search:
    purl inspect 'http://localhost:8080/res/v1/web/search?q=rust'
    purl -v --max-amount 10000 'http://localhost:8080/res/v1/web/search?q=rust'
    The server returns the settled tx hash in the PAYMENT-RESPONSE response header, but purl does not print it. Look the payer's USDC transfer up on sepolia.basescan.org instead.

Paying for a search on Tempo Moderato

No facilitator here: the server talks to a Tempo RPC endpoint directly, and verifying an MPP credential is what settles it. Only the payer needs funding.

  1. Write the config to .env and start the server (ENABLED_RAILS=mpp runs the MPP rail alone, so no facilitator config is needed; ALLOW_TESTNET is required because Moderato is a testnet):
    echo "BRAVE_SEARCH_API_KEY=<your-key>" >> .env
    echo "ENABLED_RAILS=mpp" >> .env
    echo "MPP_RPC_URL=https://rpc.moderato.tempo.xyz" >> .env
    echo "MPP_SECRET_KEY=$(openssl rand -hex 32)" >> .env
    echo "ALLOW_TESTNET=true" >> .env
    pnpm install && pnpm build && pnpm start
  2. Create a throwaway payer and fund it from the faucet RPC method:
    export MPPX_PRIVATE_KEY="0x$(openssl rand -hex 32)"
    curl -s -X POST https://rpc.moderato.tempo.xyz -H 'content-type: application/json' \
      -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tempo_fundAddress\",\"params\":[\"<payer>\"]}"
    new_payer in .github/scripts/e2e/mpp/lib.sh does this end to end, deriving the address and waiting for the balance to land.
  3. Pay for a search:
    npx mppx --network testnet -i 'http://localhost:8080/res/v1/web/search?q=rust'
    MPP clients print the Payment-Receipt header, so look the transaction up at https://explore.testnet.tempo.xyz/receipt/<transaction>.

Endpoints

Every endpoint below is payable on both rails. Prices track Brave's published rates at cost, in base units (5000 = $0.005). Brave's rate card names a price for Web Search and LLM Context, Autosuggest, and Spellcheck; the other search endpoints are charged the Web rate.

Endpoint Base units Brave rate
/res/v1/web/search 5000 $5/1k
/res/v1/llm/context 5000 $5/1k
/res/v1/news/search 5000 $5/1k
/res/v1/videos/search 5000 $5/1k
/res/v1/images/search 5000 $5/1k
/res/v1/summarizer/search 5000 $5/1k
/res/v1/local/place_search 5000 $5/1k
/res/v1/local/pois 5000 $5/1k
/res/v1/local/descriptions 5000 $5/1k
/res/v1/suggest/search 500 $5/10k
/res/v1/spellcheck/search 500 $5/10k

A path outside this table is a 404, never a payable 402, so the proxy forwards only the endpoints it sells. The Answers API (/res/v1/chat/completions) is not among them: it bills per query and per token, which one fixed price in a 402 cannot express.

A payment is checked against the price of the path it is sent to, so a credential bought for Autosuggest does not pay for a web search.

Networks

Both rails charge the same price for the same endpoint, to the same treasury, in base units of a six decimal token. See Endpoints for the per-endpoint price.

Rail Mainnet Testnet Asset
x402 Base, eip155:8453 Base Sepolia, eip155:84532 USDC
MPP Tempo, chain 4217 Moderato, chain 42431 pathUSD

ALLOW_TESTNET=true admits testnets. x402 then advertises the Base Sepolia offer first, so a client taking the first offer it supports pays with faucet money. MPP discovers its chain from MPP_RPC_URL at startup and refuses to start on a testnet without the variable.

ENABLED_RAILS picks which rails the deployment serves, as a comma-separated subset of x402,mpp; unset enables both. A disabled rail's variables are not read, its challenge is not advertised, and a payment attempt on it gets the plain 402 naming the rails that remain. ENABLED_RAILS=none serves no rails at all: the service stays up and answers every payment attempt with a 402 offering nothing, which suspends payments without taking the proxy down.

x402 verifies (a dry run that moves nothing), runs the search, then settles, so a failed search is never charged and a returned result always means the payment settled. An MPP credential is a signed transaction, so verifying it settles it, before the search runs.

End-to-end tests

Client x402 (Base Sepolia) MPP (Moderato)
purl
@x402/fetch
mppx
tempo request

mppx pays either rail. It reaches x402 through its own protocol adapter, which reads the PAYMENT-REQUIRED header and answers in PAYMENT-SIGNATURE, so that leg settles through the facilitator like any other x402 payment. The mppx entry in extensions carries the route binding it requires before it will sign.

Restricted-address screening

The proxy can refuse payments from prohibited addresses by checking each payer against a restricted-address list kept in a private S3 bucket (one HeadObject per lookup). Screening is off by default and is controlled by a single variable:

  • RESTRICTED_ADDRESS_S3_BUCKET (optional): the bucket holding the list. Unset or empty turns screening off. When set, the service probes the bucket once at startup and refuses to start if it cannot reach it, so it never serves traffic with a broken screener.

AWS credentials and region come from the standard AWS resolution chain (AWS_PROFILE, AWS_REGION, ~/.aws, or an attached IAM role), so no AWS keys live in the app config. The startup banner reports the outcome:

restricted address screening: ✓ enabled (bucket=<name>)
restricted address screening: ✗ disabled (RESTRICTED_ADDRESS_S3_BUCKET not set)

Metrics

Prometheus metrics are served on port 8090, on a listener of its own; 8080 keeps serving traffic and GET /health. Keep 8090 inside the network, since the exposition names every paid endpoint, how often payments are refused and why, and how much has been charged. Scrape localhost:8090/metrics for the full set: every series is prefixed bx402_, and one with nothing recorded yet is absent rather than zero.

About

Micropayment proxy for Brave Search API over x402 and MPP

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages