create-mcpay
v0.7.1
Published
Cloudflare Worker template for pay-per-call agent gateways: x402 signup, mcent pricing, scoped keys, XP leaderboard, MCP server, Agent Readiness.
Maintainers
Readme
create-mcpay
A reusable Cloudflare Worker template for pay-per-call agent gateways. Spin up a fully-featured agent API — auth, billing, reputation — in ~2 minutes.
mcpay — because every call costs a few mcents (1 mcent = 1/1000¢). Agents pay in crypto via x402, you keep the revenue.
Quickstart
npx create-mcpay my-api
cd my-api
npm install
wrangler secret put ADMIN_KEY # random 32-hex; required for /v1/admin/mint
wrangler deploy # Durable Object migration runs automaticallyMint your first key:
curl -X POST https://<your-worker>.workers.dev/v1/admin/mint \
-H "X-Admin-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"balance_mcents": 10000, "scopes": ["example","read"]}'
# → {"ok":true,"key":"mcp_...","balance_mcents":10000,"scopes":["example","read"]}Use it:
curl -X POST https://<your-worker>.workers.dev/v1/example \
-H "Authorization: Bearer mcp_..." \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'Security posture
- Bearer tokens stored HASHED (SHA-256). Raw tokens live only at mint time + in the holder's memory. A KV/DO dump exposes no live keys.
- Atomic charging via Durable Object with
blockConcurrencyWhile— no TOCTOU overdraft under burst traffic. - Default-deny scopes — a key minted without an explicit
scopesarray cannot call any paid endpoint. No implicit all-access. - Admin mint:
timingSafeEqualon the admin key,crypto.getRandomValuesfor the token, requiredscopes,MAX_MINT_MCENTSceiling ($1,000 default). - Validate-before-charge — handlers parse and shape-check the body before debiting. Malformed requests return 400 with
"note":"no charge applied". - No CORS on
/v1/admin/*— a compromised browser tab can't mint keys even with a leaked admin key. - Bounded body reads (16 KB default) to prevent memory DoS.
See src/template.ts header comment for the full list.
Architecture
┌──────────────┐ Bearer mcp_<hex> ┌────────────────────────────────┐
│ External │───────────────────────▶│ Cloudflare Worker (this repo) │
│ Agent │ │ /v1/example (paid, 100mc) │
└──────────────┘ │ /v1/admin/mint (admin-only) │
│ │
│ ┌────────────────────────┐ │
│ │ LeaderboardDO │ │
│ │ - atomic charging │ │
│ │ - stores hashed keys │ │
│ │ - blockConcurrency… │ │
│ └────────────────────────┘ │
└────────────────────────────────┘Extending with your own paid endpoint
async function handleMyTool(req: Request, env: Env): Promise<Response> {
// 1. Validate body first — no charge on malformed requests.
const raw = await req.text();
if (raw.length > 16 * 1024) return error(413, "body too large");
let body: any;
try { body = JSON.parse(raw); } catch { body = null; }
if (!body || typeof body.query !== "string") {
return error(400, 'missing "query"', { note: "no charge applied" });
}
// 2. Auth + charge. Atomic via the Durable Object.
const auth = await authAndCharge(req, env, 250, "mytool");
if (auth instanceof Response) return auth;
// 3. Do the work.
const result = await doTheActualThing(body.query);
return json({ ok: true, result, balance_mcents: auth.record.balance_mcents });
}Register it in the router:
if (p === "/v1/mytool" && req.method === "POST") return handleMyTool(req, env);Add to CallType, PRICE_MCENTS, XP_AWARD, SCOPE_FOR, plus a scope string. The template's SCOPE_FOR table is the single source of truth — every paid handler routes through it.
What's NOT in the template (by design)
- x402 signup, leaderboard UI, MCP server, Agent Readiness
.well-knownroutes — these are product-specific; see data-label-factory'sagent-gateway/for a reference that bolts them on. - Refund policy — depends on your failure modes. DLF's reference has a 5xx + rate-capped refund policy you can adapt.
- Rate limiting on
/v1/admin/mint— use Cloudflare's[[unsafe.bindings]]rate-limiter or a DO counter. The template assumes admin is trusted.
MPP signup — autonomous key minting
Enable /v1/signup so agents self-serve keys without a human admin. Set at least one payment method's secrets:
# Tempo (stablecoin, sub-second settlement)
wrangler secret put TEMPO_RECIPIENT # wallet to receive USDC
wrangler secret put TEMPO_CURRENCY # USDC token address on Tempo network
wrangler secret put MPP_SECRET_KEY # openssl rand -hex 32
# Stripe (card/wallet — Machine Payments must be enabled on your Stripe account)
wrangler secret put STRIPE_RECIPIENT
wrangler secret put STRIPE_NETWORK_ID
wrangler secret put STRIPE_SECRET_KEY
# Optional tuning (defaults shown)
wrangler secret put SIGNUP_PRICE_CENTS # 10 ($0.10)
wrangler secret put DEFAULT_SIGNUP_BALANCE_MCENTS # 10000 (100 calls at 100mc each)
wrangler secret put DEFAULT_SIGNUP_SCOPES # exampleThe signup flow (MPP charge intent, x402-compatible):
Agent POST /v1/signup
→ 402 WWW-Authenticate: Payment ... (one header per configured method)
Agent pays (Tempo USDC or Stripe card), retries:
→ POST /v1/signup Authorization: Payment <credential>
→ 200 {"ok":true,"key":"mcp_...","balance_mcents":10000,"scopes":["example"]}
Payment-Receipt: ...Both methods can be active simultaneously — the agent picks whichever it supports. x402 clients work unchanged (MPP is backwards-compatible with x402).
Changelog
- 0.7.0 — 5 correctness fixes: (1) Non-200 mppx responses no longer fall through to mint — only explicit 200 triggers key creation. (2) Amount formula corrected: mppx expects whole token units ("0.1" for $0.10), not base units. (3) Separate DO rate-limit windows for admin (10/hr) vs signup (1000/hr) — paid signups can't DoS the operator's admin mint. (4) compose() entries use correct
["method/intent", options]tuple format. (5)nodejs_compatflag added to generated wrangler.toml — mppx requires Node.js built-ins. - 0.6.0 — MPP signup via
/v1/signup: agents self-serve keys by paying with Tempo (stablecoin), Stripe (card), or both. UsesmppxSDK (official MPP TypeScript SDK). x402 backwards-compatible. Opt-in via secrets — no signup endpoint if neither method is configured. Strict type check onbalance_mcentsat mint: string/boolean inputs now rejected with 400 instead of being silently coerced viaNumber(). - 0.3.0 — SHA-256-hashed token storage (was: raw bearer as KV key), atomic charging via Durable Object (was: TOCTOU-prone KV rmw), admin mint balance ceiling, bounded body reads,
mcp_service-namespaced key prefix, 503 on admin when unset (no timing oracle), no CORS on admin paths. - 0.2.0 — default-deny scopes, opinionated admin mint route, validate-before-charge in example, hoisted
projectNamein scaffolder, flat 404,X-Admin-Keyremoved from CORS preflight. - 0.1.0 — initial release.
License
MIT. Fork, remix, commercialize.
