@billmyagent/x402-express
v1.1.0
Published
Charge for an Express API with x402, settled and reconciled through BillMyAgent
Readme
@billmyagent/x402-express
Charge for an Express API with x402, settled and reconciled through BillMyAgent.
npm install @billmyagent/x402-express expressimport express from 'express';
import { paymentMiddleware } from '@billmyagent/x402-express';
const app = express();
app.use(
await paymentMiddleware({
apiKey: process.env.BILLMYAGENT_API_KEY,
network: 'base',
routes: {
'GET /premium': { price: '$0.01', config: { description: 'Premium data' } },
},
})
);
app.get('/premium', (_req, res) => res.json({ ok: true }));
app.listen(4021);GET /premium now answers 402 Payment Required until a caller presents a valid
x402 payment. Any x402 client pays it automatically —
@billmyagent/payments-core, x402-axios, x402-fetch, or an agent
framework that speaks the protocol.
What this adds over x402-express
This is a thin wrapper over Coinbase's official middleware. It does two things that one cannot.
It routes the payment through BillMyAgent's facilitator, authenticated with your API key. Every payment your API takes is then recorded, checked against the chain by the reconciler, and visible in your dashboard. Without it, payments happen entirely between the buyer and Coinbase's facilitator and you have no record beyond your own logs.
It resolves your payout address from your account instead of asking you to
paste it into your server config. payTo is an address that decides where money
goes. A typo in an environment variable sends a real payment to a stranger, and
it looks like a successful transfer from every side. Reading it from the account
you registered — the same address the facilitator checks every payment against
before broadcasting — means the value can only be wrong in one place, where you
can see and fix it.
Non-custodial
The buyer's signed EIP-3009 authorization transfers USDC to your address directly. BillMyAgent never holds the funds and never appears in the transaction. The platform fee is a separate accrual against a settlement the reconciler has independently confirmed on-chain — it is not deducted from the transfer.
Timing: your handler runs before settlement
The x402 middleware verifies the payment, runs your route handler, and settles afterwards. So a settlement that fails means you already served that response for free.
This is how the protocol works and is identical with every facilitator, ours and
Coinbase's alike. For a cheap endpoint it is noise. For an expensive one, do the
cheap part in the handler and the expensive part after you have seen the
X-PAYMENT-RESPONSE header.
Sandbox keys
A sk_test_ key settles on base-sepolia only. The gateway does not offer
it a live network at all, so a sandbox key configured for base fails at
startup here — when you deploy — rather than when a buyer tries to pay.
Testnet USDC is free from faucet.circle.com. Use a
sk_live_ key to take real payments.
Networks
base and base-sepolia, settled in USDC. There is no Ethereum L1
settlement. Register a payout address for the network you want in the dashboard
under Settings → Payouts before your first paid request.
Use base-sepolia to develop against free testnet USDC.
API
paymentMiddleware(options)
Returns a Promise<RequestHandler>. Await it once at startup — the payout
address is resolved then, so a misconfigured account fails at deploy rather than
when a buyer arrives.
| Option | Default | Meaning |
| --- | --- | --- |
| apiKey | required | Your BillMyAgent API key |
| routes | required | { 'GET /path': { price: '$0.01' } }, or { '/path': '$0.01' } |
| network | 'base' | Default chain for routes that do not name one |
| baseUrl | https://api.billmyagent.ai | Gateway to talk to |
| payTo | resolved | Override the payout address and skip the lookup |
| paywall | – | Passed through to the official browser paywall page |
facilitator({ apiKey, baseUrl })
The FacilitatorConfig on its own, if you would rather call the official
paymentMiddleware yourself and keep your own payTo.
resolvePayTo({ apiKey, baseUrl, network })
The payout address registered for a network. Throws if none is registered — there is no safe fallback for "where should this money go".
Errors you might see
| Message | Cause |
| --- | --- |
| this is a sandbox API key (sk_test_), so it cannot settle on <network> | A sk_test_ key is offered testnet only. Use network: 'base-sepolia', or a sk_live_ key for real funds |
| your account has no payout address registered for <network> | Set one in the dashboard, then restart |
| could not reach the facilitator (401 …) | The API key is wrong, revoked, or its owner's email is unverified |
| A buyer sees invalid_exact_evm_payload_recipient_mismatch | The payTo in the request is not your registered address. If you passed payTo yourself, that is the mismatch |
| A buyer sees duplicate_settlement | That authorization was already settled. The client should sign a fresh one |
License
MIT
