Standard x402. Three calls.
One line of middleware, or standard x402 v2 if you already have a stack. Base, Solana and Cardano behind one API and one key. Build in a free test environment, then go live with one setting.
Test for free. Go live when you're ready.
Every site runs in test or live mode, decided by the tokens it accepts. Same endpoints, same API key, same code: going live is one setting in the dashboard.
- Money
- Test EURC and USDC from Circle's faucet, test USDM from the tUSDM faucet. Worth nothing.
- Cost
- Free. No credits needed, up to 1,000 settlements per account per day.
- Turn it on
- Select test tokens in the site's settings. A site with only test tokens shows a test mode label.
- Try it
- Press Pay with test tokens on the site page to pay a path from your browser wallet (MetaMask, Phantom, Lace…), exactly as an agent would.
- Numbers
- On the Test tab of the site's analytics, never on your bill.
- Money
- Real EURC, USDC or USDM, straight to your wallet.
- Cost
- €0.002 per successful settlement, from prepaid credits.
- Turn it on
- Select mainnet tokens in the site's settings and top up credits.
- Safety net
- Without credits, verify answers
402and your server doesn't serve paid content it can't settle. - Numbers
- On the Live tab of the site's analytics and on the monthly bill.
Going live
- In the site's settings, set a wallet for each chain, select the mainnet tokens you want and deselect the test tokens.
- Top up credits on the Billing page.
- That's it. Your integration picks up the new
acceptslist within a minute; check that the config API reports"mode": "live".
Want to keep testing after launch? Add a second site, for example staging.example.eu, that stays in test mode with its own API key. A site can also accept both at once ("mode": "mixed"); payers then choose the network.
One line in your server.
- Create a site in the dashboard, select a test token to start in test mode, and copy its site ID and API key. Keep the key on your server.
npm install @pennipay/x402and add the middleware for your framework.- Set prices per path in the dashboard. Requests without payment get a
402; paid ones reach your handler with the payment attached.
Express, Hono, Next.js, Fastify, and a fetch wrapper for Cloudflare Workers, Bun and Deno. No runtime dependencies. On WordPress or without code: see integrations.
import express from "express"; import { penni } from "@pennipay/x402/express"; const app = express(); app.use(penni({ siteId: process.env.PENNI_SITE_ID, apiKey: process.env.PENNI_API_KEY })); // Priced in the dashboard: /premium/** at €0.05 app.get("/premium/report", (req, res) => res.json({ report: "…", payer: res.locals.payment?.payer }));
import { Hono } from "hono"; import { penni, type PenniVariables } from "@pennipay/x402/hono"; const app = new Hono<{ Variables: PenniVariables }>(); app.use("/api/*", penni({ siteId: process.env.PENNI_SITE_ID, apiKey: process.env.PENNI_API_KEY })); app.get("/api/forecast", (c) => c.json({ tomorrow: "sunny", payer: c.get("payment")?.payer }));
// app/api/report/route.ts import { getPayment, withPenni } from "@pennipay/x402/next"; const options = { siteId: process.env.PENNI_SITE_ID, apiKey: process.env.PENNI_API_KEY }; export const GET = withPenni( async (req: Request) => Response.json({ payer: getPayment(req)?.payer }), options, );
// Cloudflare Workers, Bun, Deno import { getPayment, withPenni } from "@pennipay/x402/fetch"; const handler = (req: Request) => Response.json({ payer: getPayment(req)?.payer }); export default { fetch: withPenni(handler, (req, env) => ({ siteId: env.PENNI_SITE_ID, apiKey: env.PENNI_API_KEY })), };
The middleware caches prices per path for a minute, checks every payment against your current price before it reaches us, settles before your handler runs (or after a successful response, if you prefer), and handles Cardano's wait for confirmations. Already using x402 middleware? Point it at the facilitator URL https://api.pennipay.eu/v1/x402 with your site key as Bearer token.
Endpoints.
All calls go to https://api.pennipay.eu, version 1 of Penni's public API. Calls that need a key authenticate with Authorization: Bearer <site key>. Bodies are JSON. The x402 facilitator URL is https://api.pennipay.eu/v1/x402: standard x402 clients add /verify, /settle and /supported to it.
| Endpoint | What it does |
|---|---|
| GET /v1/sites/:id/config?path= | Price and ready-made x402 accepts list for one path. Without path: every price rule. Includes mode (test, live or mixed), ready and problems (no wallet, no tokens, out of credits). Cacheable for 60 seconds, supports ETags. |
| GET /v1/x402/supported | Schemes, networks and tokens the facilitator handles, and its extensions (bazaar). Public. |
| POST /v1/x402/verify | Checks a signed payment without moving money. Body: { x402Version, paymentPayload, paymentRequirements }. Free. |
| POST /v1/x402/settle | Submits the payment on-chain and returns the transaction hash. Same body. Costs €0.002 in credits when it succeeds. On Cardano it can answer settlement_pending: send the same body again. |
| GET /v1/x402/discovery/resources | The public x402 Bazaar catalog of resources from sites that opted in. Public, paginated, filterable by network and wallet. /v1/x402/discovery/search?query= searches it. |
| GET /v1/x402/discovery/price?url= | What one URL of an opted-in site costs, with the same accepts its 402 carries. Public. |
| GET /v1/health | Answers { "ok": true } while the API is up. Public. |
Versioning: new fields and endpoints are added within /v1, so ignore fields you don't know. A breaking change ships as /v2, and /v1 keeps working next to it until we announce its end well in advance.
Errors.
Penni's own checks answer with an HTTP status and { "error": "…" }. Results from the facilitator itself (isValid, success, reasons) pass through unchanged.
| Status | Meaning |
|---|---|
| 400 | The body is not valid JSON, or paymentRequirements is missing. |
| 401 | Missing or invalid site key, or a key for another site. |
| 402 | Out of credits. Top up in the dashboard; no settlement has been attempted. Never for testnet payments. |
| 403 | Site paused, payTo is not the site's wallet, or the token is not accepted for this site. |
| 409 | This Solana transaction was already settled; a payment counts once. |
| 429 | Daily limit of free test settlements reached. Resets at 00:00 UTC. |
| 502 / 503 | The facilitator could not be reached, or this server doesn't serve that chain. Safe to retry verify; for settle, check the transaction first. |
Tokens per environment.
EURC and USDC are issued by Circle; USDM on Cardano by Moneta. On Base and Solana we pay the network fee, so payers only need the token. On Cardano the payer pays it in ADA. Build on the test networks: test settlements are free, up to 1,000 a day. Your code doesn't change when you go live.
| Network | Mode | Token | Contract |
|---|---|---|---|
| Base · eip155:8453 | Live | EURC | 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 |
| Base · eip155:8453 | Live | USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Base Sepolia · eip155:84532 | Test | EURC | 0x808456652fdb597867f38412077A9182bf77359F |
| Base Sepolia · eip155:84532 | Test | USDC | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Solana · solana:5eykt4… | Live | EURC | HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr |
| Solana · solana:5eykt4… | Live | USDC | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Solana Devnet · solana:EtWTRA… | Test | EURC | HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr |
| Solana Devnet · solana:EtWTRA… | Test | USDC | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
| Cardano · cardano:mainnet | Live | USDM | c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d |
| Cardano Preprod · cardano:preprod | Test | USDM | e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9.0014df10745553444d |
Bring your own stack.
WordPress plugin
Charge AI agents for your posts while readers keep reading. Install, paste your key, done.
Cloudflare Worker
Put Penni in front of any site or API, wherever it's hosted, with a Worker you deploy in minutes.
Plain HTTP
Python, Go, PHP: fetch the price from the config API, answer 402, then verify and settle with the endpoints above. Or point existing x402 middleware at https://api.pennipay.eu/v1/x402.