{"id":"3c7cb30c-9d91-4c6c-8912-fdf40e3d4af6","slug":"hellotravisss-agentpay","kind":"mcp","name":"agentpay","summary":"Cross-rail spend-policy gateway for AI-agent payments — budgets, per-transaction limits, payee allowlists; agents never hold credentials.","owner":"Hellotravisss","repoUrl":"https://github.com/Hellotravisss/agentpay","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-06-13T05:30:34.000Z","addedAt":"2026-10-14T05:13:06.021Z","logoUrl":"https://avatars.githubusercontent.com/u/113403379?u=9d8e5aa5a010b789d7da0f420088884a67624e57&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["utilities"],"tags":["ai-agents","payments","typescript"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T13:30:49.944Z","sample":false,"url":"https://conduits.dev/tools/hellotravisss-agentpay","language":"TypeScript","transports":["stdio"],"install":{"npm":"agentpay-gateway"},"requirements":{"env":[{"name":"AGENTPAY_URL","secret":false,"required":false,"description":"agentpay gateway URL"},{"name":"AGENTPAY_API_KEY","secret":true,"required":false,"description":"Agent key from POST /admin/keys"}],"upstreamMayCharge":false},"tools":null,"readme":"# agentpay\n\n[![CI](https://github.com/Hellotravisss/agentpay/actions/workflows/ci.yml/badge.svg)](https://github.com/Hellotravisss/agentpay/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)\n![tests](https://img.shields.io/badge/tests-110%20passing-brightgreen.svg)\n\nA **cross-rail spend-policy gateway for AI agent payments**. It sits between your agents and anything that charges them money — x402-style paid APIs, Alipay/WeChat-style agent payments, whatever comes next — and answers the question every company deploying paying agents will have to answer:\n\n> *\"How do I let my agent spend money — across several payment rails — without giving it my wallets?\"*\n\nAgents never hold payment credentials. They call paid resources **through** the gateway, which:\n\n1. intercepts the `402 Payment Required` handshake and collects **every payment option the merchant accepts**,\n2. **routes to one rail** (agent's rail preference first, ties broken by cheapest cost in the policy's base currency),\n3. checks the payment against that agent's spend policy — budgets and caps are denominated in **one base currency** and enforced across all rails and currencies via exact FX conversion,\n4. executes approved payments on the chosen rail,\n5. retries the request with the payment proof and returns the unlocked response,\n6. writes **every decision — denials included — to an append-only audit log**.\n\n```\n                       ┌────────────────────────────────────────────────┐      ─▶ paid API (x402, USDC)\n  agent ──HTTP──▶      │  gateway                                       │\n  (no keys, no wallet) │  policy ─ fx ─ router ─ ledger ─ audit ─ rails │      ─▶ paid API (Alipay, CNY)\n                       └────────────────────────────────────────────────┘      ─▶ ...\n```\n\n<p align=\"center\"><img src=\"docs/dashboard.svg\" alt=\"agentpay admin dashboard — live spend vs. budgets, per-rail breakdown, and approve/reject for held payments\" width=\"860\"></p>\n\n<p align=\"center\"><sub>The built-in dashboard (<code>GET /admin</code>) — live spend vs. budgets, per-rail breakdown, and one-click approve/reject on held payments.</sub></p>\n\nFor how it's built and *why* — design principles, the full 402 request lifecycle, and where the simple parts grow — see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).\n\n## Where it fits — the neutral layer\n\nThe platforms are racing to let agents spend, and each is building a **closed loop**: Alipay's [ACT](https://www.qbitai.com/2026/01/369878.html) delegated-payment protocol, WeChat Pay's [isolated \"AI card\"](https://www.caixinglobal.com/2026-06-17/tencent-lets-ai-agent-make-purchases-through-wechat-pay-102455141.html) with user-set authorization scope and spend limits, x402 for crypto-native APIs. Every platform wants agents spending inside *its* wallet, on *its* rail — and the design they keep converging on (a wallet isolated from your real account, scoped authorization, per-spend limits, big payments held for confirmation, a full audit trail) is exactly the control surface this project is built around. The thesis is no longer speculative; it's where the giants are pointing their capex.\n\nBut a company running real agents doesn't get to live in one loop. Its agents pay an x402 API in USDC, a vendor in CNY, the next thing in whatever ships next quarter — and **no single platform gives it one neutral view across all of them**: one base-currency budget, one audit trail, one policy, one place where the agent never holds a credential. The more these closed loops fragment, the more that spanning layer is missing.\n\nThat layer is agentpay. It sits *above* the rails — x402, Alipay, WeChat, mock — behind a two-method interface ([`rails/rail.ts`](src/rails/rail.ts)), so budget, policy, FX, routing, and audit are written **once** and every rail plugs in underneath. It's rail-neutral and self-hosted by design: the value is precisely the part no platform racing to own its own loop will build for you.\n\n## Quick start\n\n```bash\nnpx agentpay-gateway      # gateway on http://127.0.0.1:4020, dashboard at /admin (mock rails)\n```\n\nFrom source:\n\n```bash\nnpm install\nnpm test        # 110 tests: money, FX, policy, x402 v1+v2, MCP, persistence, approvals, security, e2e\nnpm run demo    # walkthrough: 2 rails, 2 currencies, 1 unified USD budget, human-in-the-loop\nnpm run dev     # start the gateway on :4020 (dashboard at http://localhost:4020/admin)\n```\n\nThe demo runs a merchant that accepts USDC (x402-style) **or** CNY (Alipay-style); two agents with different rail preferences get routed differently, and one USD budget governs both — a 0.36 CNY purchase consumes ~0.0504 USD of it. Per-transaction caps, payee blocklists, budget exhaustion, the per-rail spend breakdown, and the audit trail are all shown.\n\n## Install as an agent skill\n\nThere's a ready-to-install **agent skill** in [`skill/`](skill/) — a step-by-step runbook an AI assistant (Claude Code, OpenClaw, …) follows to set agentpay up *for* you: install + self-test, write a deny-by-default policy, launch the gateway, **reroute the agent's paid calls through `/proxy`**, then operate it (spend, audit, approvals, live policy edits) and harden for production. It bundles [policy templates](skill/policies.md) and a [troubleshooting + API reference](skill/troubleshooting.md). Point your assistant at [`skill/SKILL.md`](skill/SKILL.md), or install it from a skill marketplace.\n\n## Use from Claude / Cursor (MCP)\n\nGive an MCP agent a budget it **cannot** exceed. The `agentpay-mcp` server holds only\nthe agent's gateway API key — the wallet and the limits stay in the gateway. Unlike\nhonor-system budget tools, the agent is never *asked* to check its budget; a payment\nover the limit simply isn't signed.\n\n```json\n{\n  \"mcpServers\": {\n    \"agentpay\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"-p\", \"agentpay-gateway\", \"agentpay-mcp\"],\n      \"env\": { \"AGENTPAY_URL\": \"http://127.0.0.1:4020\", \"AGENTPAY_API_KEY\": \"<key from POST /admin/keys>\" }\n    }\n  }\n}\n```\n\nTools: `paid_fetch(url, method?, body?)` — fetch any URL; the gateway pays x402/402\nwithin policy. Denials and approval holds come back as tool errors the model can\nexplain. `check_budget()` — spent today/this month vs limits (via the agent-scoped\n`GET /v1/budget`, which shows an agent only its own numbers).\n\n## Policies\n\nPolicies are deny-by-default: an agent with no policy entry cannot spend at all. See `policies/example.json`:\n\n```jsonc\n{\n  \"fxRates\": { \"USDC:USD\": \"1\", \"CNY:USD\": \"0.14\" },  // directional pairs, exact decimals\n  \"defaults\": { \"currency\": \"USD\", \"perTransactionMax\": \"1.00\", \"dailyBudget\": \"10.00\" },\n  \"agents\": [\n    {\n      \"agentId\": \"research-bot\",\n      \"enabled\": true,\n      \"currency\": \"USD\",                        // base currency for ALL limits below\n      \"railPreference\": [\"alipay-act\", \"x402\"], // routing order when merchants accept several rails\n      \"perTransactionMax\": \"0.25\",              // cap on any single payment, in base currency\n      \"dailyBudget\": \"0.15\",                    // UTC calendar day, across all rails/currencies\n      \"monthlyBudget\": \"3.00\",                  // UTC calendar month\n      \"maxTransactionsPerDay\": 200,\n      \"requireApprovalOver\": \"0.50\",            // hold payments >= this for human review (base ccy)\n      \"payeeBlocklist\": [\"merchant-shady\"]\n      // or \"payeeAllowlist\": [...] to whitelist instead\n    }\n  ]\n}\n```\n\nAll money is exact decimal (bigint micro-units, 6 dp — USDC precision); FX conversions round **up** so budgets are enforced conservatively. No floats anywhere near amounts. Payments in a currency with no configured rate to the base currency are denied (`no_fx_rate`).\n\n## API\n\n| Endpoint | Purpose |\n|---|---|\n| `ANY /proxy?url=<target>` | Proxy a request; routes + pays on 402 if policy allows. Identify the agent via `Authorization: Bearer <api key>` (when keys are configured) or `X-Agent-Id`. |\n| `GET /admin` | Web dashboard (single self-contained page) — live spend, pending approvals with approve/reject buttons, the audit trail, and inline policy editing. |\n| `GET /admin/policy` | The full live policy config. |\n| `GET /admin/agents` | Configured agents with their resolved limits. |\n| `PUT /admin/agents/:id` | Create or replace an agent's policy (validated). Takes effect on the next request; written back to the policy file. |\n| `DELETE /admin/agents/:id` | Remove an agent (reverts to deny-by-default). |\n| `GET /admin/keys` · `POST /admin/keys` · `DELETE /admin/keys/:id` | Multi-tenant API keys: list, mint (secret returned once), revoke. |\n| `GET /admin/spend/:agentId` | Unified spend vs. limits in the base currency, plus a per-rail breakdown in native currencies. |\n| `GET /admin/audit?agent=&limit=` | Audit trail of every allow/deny/failure/hold. |\n| `GET /admin/approvals?status=` | List payments held for human review (filter `pending`/`approved`/`rejected`). |\n| `POST /admin/approvals/:id` | Decide a held payment: body `{ \"decision\": \"approve\" \\| \"reject\" }`. |\n| `GET /healthz` | Liveness. |\n\nSuccessful paid responses carry `X-Gateway-Payment-Id`, `X-Gateway-Rail`, and `X-Gateway-Payment-Amount` headers. Denials return `403` with the violated rule id (`per_transaction_max`, `daily_budget`, `payee_blocklisted`, `no_fx_rate`, `approval_rejected`, ...) so the agent can explain itself or back off.\n\n### Human-in-the-loop approvals\n\nSet `requireApprovalOver` on an agent and any policy-approved payment at or above that base-currency amount is **held** instead of executed: the proxy returns `202` with an `approvalId` rather than paying. An operator approves or rejects via `POST /admin/approvals/:id`; the agent's retry then executes (approved) or is denied with `approval_rejected`. Each approval is single-use, so it unlocks exactly one payment. Budgets are still re-checked at execution time on the retry.\n\n### Persistence\n\nBy default the ledger, audit log, and approvals live in memory. Point any of them at a file to make state survive restarts — `.sqlite` selects a transactional SQLite backend (Node's built-in `node:sqlite`, no extra dependency), anything else is append-only JSONL:\n\n```bash\nLEDGER_FILE=./ledger.sqlite AUDIT_FILE=./audit.jsonl APPROVALS_FILE=./approvals.sqlite APIKEYS_FILE=./keys.sqlite npm run dev\n```\n\n### Live FX rates\n\n`FixedRateProvider` is fine for static pairs; `CachingRateProvider` wraps any async rate source (a `RateFetcher`, default `httpRateFetcher` hits exchangerate.host) with a TTL and a hard staleness limit. Past that limit a cached rate is refused (`no_fx_rate`) rather than used — a payment is never priced on a stale rate. Stablecoin pegs can be `pinned` so they never expire or fetch.\n\n### Editing policies (live)\n\nPolicies change at runtime — no restart. The `PolicyManager` owns the live config; every decision reads through it, so an edit takes effect on the next request. Two ways in, kept in sync:\n\n- **Admin API / dashboard** — `PUT`/`DELETE /admin/agents/:id` (the dashboard's edit pencils and \"add agent\" button drive these). Each edit is validated, audited as `policy_changed`, and written back to the policy file.\n- **The file itself** — `npm run dev` watches the policy file and hot-reloads out-of-band edits. The write-back and the watcher don't fight: `reload()` no-ops when the file is byte-identical to the in-memory state, so a self-write doesn't trigger a reload loop. (`fxRates` are built once at startup and not hot-reloaded.)\n\n### Multi-tenant API keys\n\nAgents authenticate with `Authorization: Bearer <key>`. Keys are minted per agent (an agent can hold several, for rotation) via `POST /admin/keys` or the dashboard — the **raw secret is returned exactly once**; only its SHA-256 hash is stored, so a leaked store yields no usable credentials. Keys support an optional expiry and can be revoked instantly (`DELETE /admin/keys/:id`); both are audited (`apikey_created` / `apikey_revoked`).\n\nBy default a valid Bearer key authenticates *and* `X-Agent-Id` still works (convenient for local dev). Set `REQUIRE_API_KEY=1` (or `requireApiKey: true`) to reject `X-Agent-Id` so only a valid key authenticates. Persist keys across restarts with `APIKEYS_FILE=./keys.sqlite`.\n\n## Payment rails\n\nRails implement one interface (`src/rails/rail.ts`): `supports(network)` + `pay(ctx) -> receipt`. Adding a rail never touches policy, FX, or routing code. Included:\n\n- **`MockRail`** — instant in-process settlement; instantiate several to simulate a multi-rail deployment.\n- **`X402Rail`** — adapter for [Coinbase's x402](https://www.x402.org/) protocol, with a **real client-side payment implementation**: `createEip3009Signer` produces signed EIP-3009 `transferWithAuthorization` payloads for USDC on Base / Base Sepolia. Speaks **both x402 v1 and v2**: v1 (`X-PAYMENT`, body requirements) and v2 (`PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` headers, CAIP-2 networks like `eip155:84532`, atomic `amount`, the chosen requirement echoed verbatim as `accepted`). Permit2-only requirements are refused rather than mis-signed. Signing is fully offline; the merchant's facilitator settles on-chain. Verified two ways: signatures are checked cryptographically in the test suite, **and the rail has settled a real payment on Base Sepolia end to end** (see below).\n- **`AlipayActRail`** — adapter shaped for Alipay's agent-payment stack (AI付 under the ACT delegation model); bring your merchant integration via `executePayment`.\n\nCredentials live inside the rail callbacks you supply — the gateway core never touches keys. Planned: Google AP2.\n\n### Buying a real x402 resource on Base Sepolia\n\nThis has been run for real: the gateway settled **0.01 USDC on Base Sepolia** through Coinbase's hosted testnet facilitator from a wallet holding zero ETH (the facilitator pays gas — x402 is gasless for the payer). On-chain proof: [`0x3108b5…54ff9d`](https://sepolia.basescan.org/tx/0x3108b58475338d6dc2aa113642b39c0128bfcbd711bff2bdb06691b76154ff9d) (x402 v1). **x402 v2** was then settled the same way against Coinbase's official `@x402/express` 2.27 merchant: [`0x8c0256…f668c5`](https://sepolia.basescan.org/tx/0x8c0256c2feb143dcb9b159c86200064c11aec03049feb08f33b6101c70f668c5).\n\nTo reproduce — public testnet endpoints come and go, so the reliable path is a local x402 merchant:\n\n```bash\n# 1. fund a throwaway wallet with testnet USDC: https://faucet.circle.com (Base Sepolia)\n\n# 2. run a local x402 merchant settling on base-sepolia (official middleware + hosted facilitator)\nmkdir x402-merchant && cd x402-merchant && npm init -y && npm i x402-express express\ncat > server.mjs <<'JS'\nimport express from \"express\";\nimport { paymentMiddleware } from \"x402-express\";\nconst app = express();\napp.use(paymentMiddleware(\"0xYourMerchantAddress\", {       // any address — it just receives the USDC\n  \"/premium\": { price: \"$0.01\", network: \"base-sepolia\", config: { description: \"x402 test\" } },\n}));\napp.get(\"/premium\", (_q, r) => r.json({ message: \"unlocked by a real on-chain payment\" }));\napp.listen(4021, () => console.log(\"merchant on http://localhost:4021/premium\"));\nJS\nnode server.mjs\n\n# 3. buy it through the gateway (signs EIP-3009 offline, facilitator settles on-chain)\nX402_PRIVATE_KEY=0x... TARGET_URL=http://localhost:4021/premium npx tsx demo/x402-live.ts\n```\n\nOr point `TARGET_URL` at any live x402 resource that settles on `base-sepolia`.\n\n## Security\n\nIt moves money, so the control plane is hardened, not just the math:\n\n- **Admin API auth** — set `ADMIN_TOKEN` and every `/admin/*` data/mutation endpoint requires it (`Authorization: Bearer` or `X-Admin-Token`, constant-time compared). Unset, the admin API is open, so the CLI **binds `127.0.0.1` by default** (override with `HOST`) and warns at startup. The dashboard carries the token (seed it once via `/admin#token=…`).\n- **SSRF guard** — `/proxy` refuses targets that resolve to private/loopback/link-local ranges (incl. the `169.254.169.254` cloud-metadata IP) by default. The guard runs at connect time via a pinned DNS lookup and is re-applied to **every redirect hop**, so neither DNS rebinding nor a redirect-to-internal can slip past. Opt in with `allowPrivateTargets` only for local testing.\n- **x402 asset allowlist** — the signer refuses to sign an EIP-3009 authorization for any token not on a per-network allowlist (default: canonical USDC), so a malicious `402` can't trick the wallet into authorizing a transfer of a different, more valuable token. Authorization validity is clamped (`maxAuthorizationSeconds`).\n- **No double-spend under concurrency** — the decide→execute→record window is serialized per agent, so concurrent payments can't both pass the budget check before either is recorded.\n- **DoS limits** — 1 MiB request-body cap (`413`), 30 s upstream fetch timeout, and the upstream response is streamed (not buffered whole into memory).\n- **Money integrity** — exact `bigint` arithmetic (no floats), API keys stored only as SHA-256 hashes (raw secret shown once), and the upstream `Authorization` header is never forwarded.\n\n**Deploy checklist:** set `ADMIN_TOKEN`, enable `REQUIRE_API_KEY=1`, keep `allowPrivateTargets` off, and put the gateway behind TLS. Full threat model, hardening list, and checklist in [docs/SECURITY.md](docs/SECURITY.md).\n\n## Status & roadmap\n\nThis is an MVP. The policy engine, FX layer, cross-rail router, ledger, audit log, approvals, persistence, and 402 proxy flow are tested and working end to end against mock rails; the x402 rail has settled a real payment on Base Sepolia. Done since the first cut:\n\n- [x] Live FX rate provider with caching and staleness limits (`CachingRateProvider`)\n- [x] Persistent storage beyond JSONL — transactional SQLite via `node:sqlite`\n- [x] Human-in-the-loop approvals (\"hold payments over $X for review\")\n- [x] Web dashboard for spend + audit + approvals + policy editing (`GET /admin`)\n- [x] Policy hot-reload and an admin API for editing policies\n- [x] Multi-tenant API key management (hashed keys, mint/revoke, per-key expiry)\n- [x] End-to-end x402 v2 settlement against the official v2 merchant — [0.01 USDC on Base Sepolia](https://sepolia.basescan.org/tx/0x8c0256c2feb143dcb9b159c86200064c11aec03049feb08f33b6101c70f668c5)\n- [x] End-to-end x402 settlement against a live facilitator — [0.01 USDC settled on Base Sepolia](https://sepolia.basescan.org/tx/0x3108b58475338d6dc2aa113642b39c0128bfcbd711bff2bdb06691b76154ff9d) (reproduce: [above](#buying-a-real-x402-resource-on-base-sepolia))\n\nNot yet built:\n\n- [ ] Real Alipay AI付/ACT settlement (requires merchant onboarding)\n"}