Three weeks ago I started building a payment proxy for AI agents. The idea was simple: agents deposit USDC once, and Cinderwright handles all the x402 signing so they don't have to.
What I didn't expect was spending three full sessions just getting the payment signature to work against real services.
Here's everything I learned, and the shortcut that makes all of it unnecessary.
The x402 Payment Flow (What the Docs Say)
On paper, it's elegant:
- Agent calls a service → gets HTTP 402 with
payment-requiredheader - Agent decodes the base64 header, reads the payment requirements
- Agent signs an EIP-3009 authorization
- Agent retries with
payment-signatureheader - Service verifies via facilitator, returns data
The official docs show this working in a few lines. What they don't show: the four different ways it can silently fail.
Problem 1: The accepted Field Nobody Mentions
The PayAI facilitator (used by Bazaar, scoutgate, and others) requires a specific field in your payment payload called accepted. This is the payment requirement object you selected from the accepts array.
Without it:
{"isValid": false, "invalidReason": "invalid_payload", "invalidMessage": "accepted: Invalid input"}
With it:
const rawPayload = await scheme.createPaymentPayload(x402Version, requirement, null);
const fullPayload = {
...rawPayload,
accepted: requirement // ← This. Without this, nothing works.
};
Problem 2: The Network String Mismatch
Services send "eip155:8453" in their 402 challenge.
The x402/schemes package (the newer one) only understands "base".
The @x402/evm package (the older one) understands "eip155:8453".
These are different packages. If you import the wrong one, you get:
Error: Unsupported network: eip155:8453
The fix: use @x402/evm for services that send eip155:8453 in their challenge. Or normalize manually before signing and restore the original string before sending.
Problem 3: The Wrong Header Name
I tried X-PAYMENT. Then PAYMENT-SIGNATURE. Then x-payment.
The correct header is payment-signature (lowercase). This is what @x402/express actually reads on the server side:
const header = adapter.getHeader("payment-signature") || adapter.getHeader("x-payment");
Both work. But X-PAYMENT alone does not.
Problem 4: The Facilitator Doesn't Support Mainnet
x402.org's facilitator (the free one) only supports testnet. For mainnet services (Bazaar, ScoutGate, etc.), you need the PayAI facilitator at https://facilitator.payai.network.
Our server was configured to use https://facilitator.payai.network but the verify endpoint expects the full payment requirements object, not just the single accepts entry.
What Actually Works (Full Working Example)
import { ExactEvmScheme, toClientEvmSigner } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
import { createWalletClient, createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const wc = createWalletClient({ account, chain: base, transport: http() });
const pc = createPublicClient({ chain: base, transport: http() });
wc.address = account.address;
const signer = toClientEvmSigner(wc, pc);
const scheme = new ExactEvmScheme(signer);
// Step 1: Get 402
const r1 = await fetch(serviceUrl);
const payReqs = JSON.parse(Buffer.from(r1.headers.get('payment-required'), 'base64').toString());
const requirement = payReqs.accepts[0];
// Step 2: Sign
const rawPayload = await scheme.createPaymentPayload(payReqs.x402Version || 2, requirement, null);
const fullPayload = {
...rawPayload,
accepted: requirement // ← The missing field
};
const encoded = Buffer.from(
JSON.stringify(fullPayload, (k, v) => typeof v === 'bigint' ? v.toString() : v)
).toString('base64');
// Step 3: Retry
const r2 = await fetch(serviceUrl, {
headers: { 'payment-signature': encoded } // ← lowercase
});
// Done. Data arrives.
That's the full working implementation. About 25 lines.
The Shortcut
If you don't want to deal with any of this, there's a proxy:
# 1. Get a key (one time)
curl -X POST https://api.ideafactorylab.org/proxy/setup \
-H "Content-Type: application/json" \
-d '{"wallet": "0xYourBaseWallet"}'
# 2. Deposit some USDC to the address it returns
# 3. Call any x402 service
curl "https://api.ideafactorylab.org/proxy?url=https://bazaar-gateway.vercel.app/api/weather" \
-H "X-CW-Key: sk_cw_yourkey"
That's it. The proxy handles signing, the accepted field, the facilitator format, the header name, and auto-failover if a service is down. You just get the data.
The proxy currently covers 1,686 x402 services. It charges a 10% markup. No gas management required.
Discovery
The same project also indexes all x402 services (plus MPP and Lightning) so you can find them:
# Free, no payment needed
curl "https://api.ideafactorylab.org/discover?q=weather"
curl "https://api.ideafactorylab.org/quality" # A-F grades on 2,771 services
MCP server for Claude Desktop:
npx -y cinderwright-mcp-server
If you've hit any of these issues yourself, drop a comment — curious whether the accepted field problem was widespread or just a few services.
GitHub: https://github.com/cinderwright-ai/cinderwright-api
Proxy docs: https://api.ideafactorylab.org/proxy
Top comments (0)