Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (create-mcpay) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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.