Receipts and accounting for agent spending. Turn a bare settlement id — a Base transaction hash or a Solana transaction signature — into a signed receipt an accountant will accept, and export the whole book as CSV.
npm install x402-receipts
An x402 payment succeeds and your agent gets this back:
X-PAYMENT-RESPONSE: { "success": true, "network": "base-sepolia", "transaction": "0x33b7dd08…" }
That is not a receipt. No amount, no counterparty, no timestamp, no idea what was bought. At month end you have a few thousand of them across two chains and a finance function that would like a CSV.
| Route | Price | What you get back |
|---|---|---|
GET /receipt/:tx | $0.001 | Enriched, signed receipt: payer, payee, amount, asset, block time, fee, every transfer, your metadata |
GET /export | $0.01 | The full ledger as CSV and JSON in one response, with per-rail and per-merchant totals |
POST /annotate/:tx | free* | Attach resource / merchant / label / category |
GET /summary | free | Totals without the rows — decide whether the export is worth buying |
POST /verify | free | Signature check on any receipt or export |
*requires X-Admin-Key. The export returns both formats deliberately: CSV is what accounting software imports, JSON is what the next agent parses. Charging twice for the same rows would be a worse product.
The identifier's shape picks the chain. No flag, no guessing:
# Base: 0x + 64 hex
curl "localhost:4033/receipt/0x33b7dd081487769518a62d6da79f1e9f2872c02253b0981ac6317fc0c51c65e2"
# Solana: base58 signature
curl "localhost:4033/receipt/qMvBwBSdfWTAPyDZi1BDk3Nu68HEVAsz9aMexw3XT7rLobYmpLSg1BBW9Bgvpt1e7A8NUjcuSjAZ5aoYF8cARjj"
Both come back in the same shape. Getting there takes genuinely different work:
Read the transaction receipt and decode every Transfer(address,address,uint256) log. The event states sender, recipient and amount directly.
No transfer event exists. Diff preTokenBalances against postTokenBalances per (owner, mint) — negative deltas are senders, positive ones receivers. Survives transferChecked, CPI wrappers and multi-hop routes, where instruction parsing does not.
One Solana subtlety: on x402 payments the fee payer is the facilitator's sponsor, not the buyer — that is what lets a buyer pay with USDC and no SOL. Read payer from the transfer, never the fee payer.
import { resolveTransaction, ReceiptLedger, toCsv } from "x402-receipts";
const ledger = new ReceiptLedger({ file: "data/receipts.json" });
const receipt = await resolveTransaction("0x33b7dd08…"); // Base
const solana = await resolveTransaction("qMvBwBSd…"); // Solana
ledger.record(receipt, { resource: "https://api.example.com/report", category: "data" });
ledger.record(solana, { resource: "https://api.example.com/report", category: "data" });
ledger.summary();
// { count: 2, totalUsd: 0.02, byRail: { evm: {…}, solana: {…} }, … }
toCsv(ledger.list({ since: "2026-08-01" }));
Pleasingly circular: the transaction in your X-PAYMENT-RESPONSE is exactly what you would feed back into GET /receipt/:tx.
Every receipt comes from a live chain read through public, keyless RPC — Base via viem, Solana via api.mainnet-beta.solana.com. Set BASE_RPC_URL / SOLANA_RPC_URL to a dedicated provider before you depend on it; the public endpoints are rate-limited.
Run it, resolve a real Base tx and a real Solana signature, buy the export.
Every route and every library export, with schemas and error cases.
Discovery, both rails, MCP integration, and where to get listed.
The agent-facing contract in one file.
Machine-readable manifest for x402 indexes.
Including the 402 response and PaymentRequirements schema.