Autonomous agent commerce — buy gift cards, top up phones, get eSIMs with USDC on Base (Ethereum L2) via x402 protocol. No account, no OAuth, no API key. For AI agents with crypto wallets...
Fully autonomous purchasing for AI agents with crypto wallets. No accounts, no OAuth, no API keys — just stablecoin and a signing key.
Built on the x402 protocol — HTTP 402 Payment Required as a machine-to-machine payment standard. Two settlement rails are supported: Base (USDC on Ethereum L2, default) and Solana (USDC SPL on Solana mainnet, opt-in).
For human-guided purchases with multiple payment methods, follow cryptorefills-buy instead. For browse-only catalog exploration, follow cryptorefills-catalog instead.
| Rail | Host | Phase-1 header | Wallet | Asset | Native gas |
|---|---|---|---|---|---|
| Base (default) | x402.cryptorefills.com |
none, or X-Preferred-Network: base |
EVM, EIP-712 capable | USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, chain 8453 |
None — Cryptorefills relays |
| Solana | x402.cryptorefills.com |
X-Preferred-Network: solana |
Solana keypair | USDC SPL mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
None — CDP facilitator pays SOL |
| Solana (host-pinned) | solana.x402.cryptorefills.com |
omit, or solana only |
Solana keypair | same | None |
Default-rail recommendation. If your agent has wallets on both rails and no external constraint, use Base — broader EIP-712 tooling, longer payment window (~15 min vs Solana's ~60 s), and the same byte-identical contract that EVM-only agents have used since v1.0. Use Solana when the agent already runs there, when SOL avoidance matters, or when a registry / marketplace (e.g., pay-skills) demands a Solana-mainnet 402 challenge by default — in that case use the solana.x402.cryptorefills.com host so the rail is host-pinned and unambiguous.
| Requirement | Details |
|---|---|
| USDC on Base | Chain 8453, contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| EIP-712 signing | Sign typed data for transferWithAuthorization (EIP-3009) |
| ETH for gas | Not required — Cryptorefills relays the authorization on-chain |
| HTTP client | Standard REST API calls |
| Account | None required |
| Requirement | Details |
|---|---|
| USDC SPL on Solana mainnet | Mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, 6 decimals |
| Solana keypair | 64-byte Ed25519 secret (Phantom / Solflare export format works) |
| Solana RPC access | For fresh recentBlockhash. Default https://api.mainnet-beta.solana.com; any reliable RPC works |
| v0 transaction signing | Build a VersionedTransaction with the canonical 3-instruction shape and partial-sign — see protocol.md |
| SOL for gas | Not required — the CDP facilitator pays SOL gas from its pool |
| HTTP client | Standard REST API calls |
| Account | None required |
Autonomous agents spending real money require strict controls:
order_id, product, amount, wallet address, rail, tx signature/hash.maxAmountRequired in the 402 response.payTo is a per-session deposit wallet — the manifest does not list deposit wallets, so an allowlist is impossible. Instead every 402 carries X-Payment-Required-Signature, a JWS (ES256) signed by the gateway. Verify it with the keys from the manifest's attestation.keys / attestation.jwks_uri (same origin as the manifest) and require: iss == the origin you POSTed to, sid == the X-Session-Id header, pr_sha256 == base64url(sha256(PAYMENT-REQUIRED header value exactly as received)), pay_to/network == the decoded accepts[0] values, exp in the future. If the header is absent or any check fails, reject the 402 and do not sign. Full recipe + verify402() snippet: references/protocol.md → Verifying the 402 attestation.Solana-specific:
payTo is the OWNER pubkey, NEVER an ATA. Derive the recipient ATA from (payTo, mint). Signing a transfer treating payTo as an ATA will fail.extra.feePayer matches the value from PAYMENT-REQUIRED before partial-signing — and use it as the transaction's payerKey.recentBlockhash immediately before partial-signing. The validity window is ~60 s; the gateway clamps maxTimeoutSeconds to 60 to flag this. Do not insert user prompts between Phase 1 and Phase 2 on Solana.TransferChecked (tag 0x0c), not Transfer (0x03). Plain Transfer is rejected by CDP with unknown_error.General:
voucher_code) are cash-like — store securely, never share publicly or write to logs.Manifests are served at /.well-known/x402.json on each host and carry an attestation block (signing keys, jwks_uri, verify procedure). Read the manifest at the same host you used for Phase 1 — the attestation's iss must equal that origin, and jwks_uri must be same-origin with it.
Always send Phase 2 to the same host as Phase 1. The session backing X-Session-Id is host-affine; cross-host POST risks a session lookup miss.
GET /v1/brands → discover brands
GET /v1/catalog → get products and prices
POST /v1/orders → 402 + PAYMENT-REQUIRED + X-Session-Id + X-Payment-Required-Signature
↓ verify the attestation (JWS) — reject the 402 if it is missing or invalid
↓ sign (EIP-712 for Base, partial-sign v0 tx for Solana)
POST /v1/orders → submit with PAYMENT-SIGNATURE [+ X-Session-Id] → 200
GET /v1/orders/{id} → poll until delivered
The flow, REST contract, and order/delivery model are identical on both rails. Only Phase-1 header selection, the extra block on PAYMENT-REQUIRED, and the signing primitive differ.
GET /v1/brands?country_code=us
Returns available brands for the country. Each entry includes brand_name, family, category, min, max.
Country codes are lowercase ISO 3166-1 Alpha-2 (us, it, br) regardless of rail. If receiving a country code from the catalog or buy skills (which use uppercase), convert to lowercase before using x402 endpoints.
GET /v1/catalog?country_code=us&brand_name=Amazon.com
Returns products with product_id, is_range, currency, price_usdc, and either denomination (fixed) or min_value/max_value (range).
is_range: false): use product_id directly. Omit product_value. Face value is denomination in currency units.is_range: true): set items[].product_value to the desired amount, in the brand's local currency (NOT USD). E.g. for a €5 Amazon.nl card with currency: "EUR", send product_value: 5. The 402 response will quote the USDC amount to actually settle (typically price_usdc ± a small spread).Before ordering, verify your wallet has sufficient stablecoin on the chosen rail:
balanceOf(walletAddress) on 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.getTokenAccountBalance on the sender ATA — getAssociatedTokenAddress(mint=EPjF…WEGGkZwyTDt1v, owner=agent). No SOL balance needed.The price_usdc from the catalog is indicative; the exact amount is confirmed in the 402 response.
Base (default):
curl -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-d '{
"email": "delivery@example.com",
"items": [{
"product_id": "abc123",
"beneficiary_account": "recipient@example.com",
"product_value": 25
}]
}'
# → HTTP 402 with PAYMENT-REQUIRED, X-Session-Id and X-Payment-Required-Signature headers
Solana (header opt-in):
curl -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-H "X-Preferred-Network: solana" \
-d '<same body>'
Solana (host-pinned):
curl -X POST https://solana.x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-d '<same body>'
product_value is required for range products; omit for fixed-denomination products. Optional: add "callback_url": "https://..." for webhook delivery notifications instead of polling.
Capture three response headers:
PAYMENT-REQUIRED — base64url-encoded JSON of payment requirements (per-rail shape below).X-Session-Id — UUID. Echo on Phase 2 (required for Solana, optional for Base).X-Payment-Required-Signature — JWS attesting the two headers above (iss, sid, pr_sha256, pay_to, network, exp). Verify before signing — see references/protocol.md. Treat a missing header as a failed 402.{
"x402Version": 2,
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"maxAmountRequired": "25000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": { "name": "USD Coin", "version": "2", "decimals": 6 },
"payTo": "0x...",
"description": "$25.00 Amazon Gift Card"
}],
"expiresAt": 1830000000
}
protocol.md)| Base | Solana | |
|---|---|---|
network |
eip155:8453 |
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (full CAIP-2) |
asset |
EVM contract | SPL mint, base58 (EPjF…WEGGkZwyTDt1v for USDC) |
extra |
{name, version, decimals} |
{feePayer} — use as payerKey |
payTo |
recipient wallet | recipient OWNER pubkey (ATA derived facilitator-side) |
expiresAt window |
~15 minutes | ~60 seconds |
Sign typed data for transferWithAuthorization.
Domain: USDC contract on Base.
Types: TransferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce).
| Field | Value |
|---|---|
from |
Your wallet address |
to |
payTo from 402 response |
value |
maxAmountRequired from 402 response |
validAfter |
0 |
validBefore |
expiresAt from the 402 response (Unix seconds) |
nonce |
Random 32-byte hex |
See references/protocol.md for exact EIP-712 typed data structure.
Build a v0 VersionedTransaction with exactly 3 instructions, in order: ComputeBudget set-limit (200_000 units) → ComputeBudget set-price (1000 microLamports) → SPL createTransferCheckedInstruction (tag 0x0c, NOT Transfer 0x03). Use mint = asset, senderAta = getAssociatedTokenAddress(mint, signer), recipientAta = getAssociatedTokenAddress(mint, payTo) (where payTo is the owner), payerKey = extra.feePayer, fresh recentBlockhash from a Solana RPC. Partial-sign with tx.sign([signer]) — leave the feePayer slot for CDP. Serialize ≤ 1232 bytes.
See references/protocol.md for the full recipe.
Re-POST the same body with PAYMENT-SIGNATURE. Echo X-Session-Id from Phase 1 (required for Solana).
curl -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-H "PAYMENT-SIGNATURE: <base64url-encoded JSON>" \
-d '<same body as phase 1>'
# → HTTP 200
PAYMENT-SIGNATURE payload (then base64url-encode the JSON):
{
"x402Version": 2,
"scheme": "exact",
"network": "eip155:8453",
"payload": {
"signature": "0x...",
"authorization": {
"from": "0xYourWallet",
"to": "0xPayToAddress",
"value": "25000000",
"validAfter": "0",
"validBefore": "1830000000",
"nonce": "0xRandomBytes32"
}
}
}
curl -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-H "PAYMENT-SIGNATURE: <base64url-encoded JSON>" \
-H "X-Session-Id: <UUID from Phase 1 response>" \
-d '<same body as phase 1>'
# → HTTP 200
PAYMENT-SIGNATURE payload (base64url-encode the outer JSON; the inner payload.transaction is plain base64):
{
"x402Version": 2,
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"payload": {
"transaction": "<plain-base64 of serialized v0 transaction>"
}
}
Encoding rule. The outer
PAYMENT-SIGNATUREwrapper is base64url for both rails. Only the innerpayload.transactionfield on Solana is plain base64 (Solana ecosystem convention; opaque pass-through to the facilitator). Don't unify them.
Server responds with HTTP 200:
{
"order_id": "cr_abc123",
"status": "processing",
"estimated_delivery_seconds": 60,
"poll_url": "/v1/orders/cr_abc123"
}
GET /v1/orders/{order_id}
Poll every 5–10 seconds until status is completed or failed. Stop polling after 10 minutes and report the order as potentially stuck — contact support@cryptorefills.com with the order_id.
When completed, the deliveries array contains:
voucher_code — gift card redemption codepin — PIN if requiredurl — redemption URL if applicablebrand_name, product_name — what was purchasedEnd-to-end check that the per-step instructions compose. Real numbers, anonymized wallet:
# 1. Discover Amazon.nl
curl 'https://x402.cryptorefills.com/v1/brands?country_code=nl' | jq '.[] | select(.family|test("Amazon"))'
# → {"brand_name":"Amazon.nl","family":"Amazon.nl","category":"e-commerce", ...}
# 2. Find a range product priced in EUR
curl 'https://x402.cryptorefills.com/v1/catalog?country_code=nl&brand_name=Amazon.nl' | jq '.[0]'
# → {"product_id":"amzn-nl-range","is_range":true,"currency":"EUR",
# "min_value":5,"max_value":500,"price_usdc":"5.42", ...}
# 1 EUR isn't offered (min is 5 EUR) — pick 5 EUR.
# 3. Phase 1 — opt into Solana
curl -i -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-H "X-Preferred-Network: solana" \
-d '{"email":"agent@example.com",
"items":[{"product_id":"amzn-nl-range",
"beneficiary_account":"user@example.com",
"product_value":5}]}'
# → HTTP/1.1 402
# X-Session-Id: 3f9c1a8b-... ← capture, echo on Phase 2
# PAYMENT-REQUIRED: <base64url JSON> ← decode for accepts[0]
# X-Payment-Required-Signature: <JWS> ← verify first (protocol.md → Verifying the 402 attestation)
#
# accepts[0] decoded:
# network: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
# asset: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
# maxAmountRequired: "5420000" ← 5.42 USDC
# payTo: <owner pubkey> ← derive recipient ATA from this
# extra.feePayer: <CDP pubkey> ← use as transaction.payerKey
// 4. Decode PAYMENT-REQUIRED, verify budget, build the partial-signed v0 tx
import {ComputeBudgetProgram, Connection, Keypair, PublicKey,
TransactionMessage, VersionedTransaction} from '@solana/web3.js';
import {createTransferCheckedInstruction, getAssociatedTokenAddress} from '@solana/spl-token';
import bs58 from 'bs58';
// 4a. Parse the 402 response
const paymentRequired = JSON.parse(
Buffer.from(phase1Headers['payment-required'], 'base64url').toString());
const sessionId = phase1Headers['x-session-id'];
const {payTo, asset, maxAmountRequired, extra} = paymentRequired.accepts[0];
// 4b. Spending-safeguard checks BEFORE signing
const SPEND_LIMIT_ATOMIC = BigInt(50_000_000); // $50 = 50e6 atomic USDC
if (BigInt(maxAmountRequired) > SPEND_LIMIT_ATOMIC) throw new Error('over budget');
if (asset !== 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') throw new Error('wrong mint');
// 4c. Build the transaction
const signer = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_SIGNER_KEY));
const conn = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
const mint = new PublicKey(asset);
const owner = new PublicKey(payTo); // OWNER, not an ATA
const feePayer = new PublicKey(extra.feePayer); // CDP signs this slot
const senderAta = await getAssociatedTokenAddress(mint, signer.publicKey);
const recipientAta = await getAssociatedTokenAddress(mint, owner);
const {blockhash} = await conn.getLatestBlockhash('finalized'); // 'finalized' is older but accepted by all validators — reduces timing sensitivity
const message = new TransactionMessage({
payerKey: feePayer,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({units: 200_000}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: 1000}),
createTransferCheckedInstruction(senderAta, mint, recipientAta,
signer.publicKey, BigInt(maxAmountRequired), 6),
],
}).compileToV0Message();
const tx = new VersionedTransaction(message);
tx.sign([signer]); // partial — leaves feePayer slot
// Assert on the pre-compilation array — MessageV0 exposes `compiledInstructions`, not `instructions`.
// Reading `tx.message.instructions` throws TypeError. Use the source array passed to TransactionMessage.
if (message.version !== 0) throw new Error('expected v0 message');
if (tx.message.compiledInstructions.length !== 3) throw new Error('expected exactly 3 instructions');
if (tx.serialize().length > 1232) throw new Error('tx too large');
const txB64 = Buffer.from(tx.serialize()).toString('base64'); // inner: plain base64
const inner = {x402Version: 2, scheme: 'exact',
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
payload: {transaction: txB64}};
const headerValue = Buffer.from(JSON.stringify(inner)).toString('base64url'); // outer: base64url
# 5. Phase 2 — same host as Phase 1, echo X-Session-Id from step 3
curl -X POST https://x402.cryptorefills.com/v1/orders \
-H "Content-Type: application/json" \
-H "PAYMENT-SIGNATURE: <headerValue from step 4c>" \
-H "X-Session-Id: <sessionId from step 4a>" \
-d '<same body as Phase 1>'
# → HTTP/1.1 200
# {"order_id":"cr_xxx","status":"processing","poll_url":"/v1/orders/cr_xxx", ...}
# 6. Poll
curl https://x402.cryptorefills.com/v1/orders/cr_xxx
# → {"status":"completed",
# "deliveries":[{"voucher_code":"XXXX-YYYY-ZZZZ","brand_name":"Amazon.nl", ...}]}
If steps 1–6 don't compose for your scenario, the failure is documented in references/troubleshooting.md.
If switching between x402 and MCP skills, note these field name differences:
| Concept | x402 (this skill) | MCP (cryptorefills-buy) |
|---|---|---|
| Gift card code | voucher_code |
deliverable.pin_code |
| PIN | pin (separate field) |
included in pin_code |
| Order status | status (single field) |
order_state + payment_state |
| Order ID format | cr_ prefixed |
UUID |
us, not US). MCP skills use uppercase — different endpoint, different rule./v1/brands response.X-Preferred-Network value: case-sensitive lowercase — base or solana only. Base or Solana (capitalized) returns 400 invalid_network on the multi-rail host.maxAmountRequired from 402 — do not round or modify.PAYMENT-SIGNATURE outer wrapper is base64url for both rails (matches the gateway's decoder).X-Payment-Required-Signature, or whose signature fails any check, must be rejected — never fall back to "the TLS connection looked fine". Hash the PAYMENT-REQUIRED header string as received; do not re-serialise the decoded JSON.0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 on Base.8453 (Base mainnet). Wrong chain = invalid signature.expiresAt field gives the deadline (~15 min). Sign and resubmit before it expires.solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, not "solana" or "solana-mainnet".payTo is the OWNER pubkey, not an ATA. The facilitator derives the recipient ATA from (owner, mint).TransferChecked (0x0c), not Transfer (0x03). CDP rejects the latter with unknown_error.payerKey = extra.feePayer from PAYMENT-REQUIRED. CDP signs that slot at settle and pays SOL gas.X-Session-Id from the Phase 1 response on Phase 2 — required (the SPL transaction is opaque so the gateway has no other way to look up the session).payload.transaction inside the PAYMENT-SIGNATURE JSON is plain base64 (Solana convention) — only the outer wrapper is base64url.Load references only when you need deeper detail than what's in this skill file.
| File | Load when... | Content |
|---|---|---|
references/protocol.md |
You need the exact signing recipe — EIP-712 domain/types for Base, or the full VersionedTransaction build for Solana — or the 402 attestation verifier |
Two-phase flow, headers, per-rail PAYMENT-REQUIRED shape, X-Payment-Required-Signature verification (verify402), Base EIP-712 / EIP-3009 detail, Solana 3-instruction recipe |
references/endpoints.md |
You need exact request/response schemas for an API call | REST API reference, Hosts table, full headers list, error-response codes |
references/troubleshooting.md |
A request fails or returns an unexpected status | Per-rail error catalogue and recovery patterns |