Accept payments
Charge AI agents per call in USDC for any HTTP endpoint or MCP tool
You ship a paid HTTP endpoint or MCP tool. Mailgent handles the x402 dance: serves a 402 challenge, verifies the buyer's signed USDC authorization, settles on Base, and signs an Ed25519 receipt. This page is the end-to-end seller flow.
Before you start
- A project at console.mailgent.dev with Pay → Accept turned on.
- Your
loid-...API key from the project dashboard. - The SDK:
npm install @mailgent/sdk(Node/Bun/Deno) orpip install "mailgent-sdk[fastapi]"(Python).
The five-line paywall
import express from "express";
import { Mailgent } from "@mailgent/sdk";
import { requirePayment } from "@mailgent/sdk/paywall/express";
const app = express();
app.set("trust proxy", true);
const mailgent = new Mailgent({ apiKey: process.env.MAILGENT_API_KEY! });
app.get(
"/search",
requirePayment({ amount: "0.05" }),
(req, res) => res.json({ results: [/* ... */] }),
);
app.listen(3030);That's the whole integration. Set MAILGENT_API_KEY=loid-... and run. The middleware reads the env var automatically; requirePayment does the 402 challenge and the verify-and-settle step.
Other frameworks
Same shape, different import:
import { Hono } from "hono";
import { requirePayment } from "@mailgent/sdk/paywall/hono";
const app = new Hono();
app.get("/search", requirePayment({ amount: "0.05" }), (c) => c.json({ results: [] }));import { requirePayment } from "@mailgent/sdk/paywall/mcp";
server.tool(
"search",
{ description: "Paid search" },
requirePayment({ amount: "0.05" }, async (args) => ({ results: [/* ... */] })),
);from fastapi import FastAPI, Depends
from mailgent.paywall import require_payment
app = FastAPI()
@app.get("/search", dependencies=[Depends(require_payment(amount="0.05"))])
def search():
return {"results": [...]}Without middleware (custom framework)
For Fastify, NestJS, Next.js, or any framework Mailgent doesn't ship middleware for, wire the two SDK calls into your handler:
app.get("/search", async (req, res) => {
const resource = `${req.protocol}://${req.get("host")}${req.path}`;
const xPayment = req.get("x-payment");
if (!xPayment) {
const challenge = await mailgent.payments.challenge({ amount: "0.05", resource });
return res.status(402).json(challenge);
}
const result = await mailgent.payments.redeem({
paymentHeader: xPayment, resource, amount: "0.05",
});
if (!result.ok) return res.status(402).json(result.requirement);
res.setHeader("X-Payment-Response", result.paymentResponse);
res.json({ results: [/* ... */] });
});This is exactly what requirePayment does internally. Same SDK client, no extra dependencies, works on any HTTP server.
Test locally
api.mailgent.dev fetches your URL from the public internet to drive the buyer side, so localhost won't work. Expose your server with a tunnel:
ngrok http 3030
# Forwarding https://abcd-1234.ngrok-free.app -> http://localhost:3030Two things keep the URL the buyer signs in sync:
app.set("trust proxy", true)— Express reportshttps://(nothttp://) behind a tunnel.- In console.mailgent.dev → your project → Pay → Add endpoint, register the ngrok URL (exact match: host + path).
Sanity check:
curl -i https://abcd-1234.ngrok-free.app/search
# HTTP/1.1 402 Payment Required
# { "x402Version": 1, "accepts": [{ ... }] }See payments programmatically
const recent = await mailgent.payments.list({ limit: 20 });
const one = await mailgent.payments.get("cmoh...");
console.log(one.signedReceipt); // Ed25519-signed proof of paymentSame data the Pay tab shows. Use for in-product receipts, reconciliation, or your own admin views.
Webhooks
In console.mailgent.dev → your project → Pay → Endpoints, edit the endpoint and add a webhookUrl. Mailgent generates a whsec_… signing secret and shows it once — copy it then.
Mailgent POSTs payment.received to your URL on every successful settle. Body is the Ed25519-signed receipt; X-Mailgent-Signature: sha256=<hex> is HMAC-SHA256 over the raw body. Companion headers: X-Mailgent-Event, X-Mailgent-Idempotency-Key (de-dupe on this).
import express from "express";
import { verifyWebhook } from "@mailgent/sdk/webhook";
app.post(
"/webhooks/mailgent",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = await verifyWebhook(
req.body.toString(),
req.header("x-mailgent-signature"),
process.env.MAILGENT_WEBHOOK_SECRET!,
);
if (!ok) return res.status(400).send("invalid signature");
// de-dupe on x-mailgent-idempotency-key, then handle the receipt
res.sendStatus(200);
},
);from mailgent.webhook import verify_webhook
@app.post("/webhooks/mailgent")
async def webhook(request: Request):
raw = await request.body()
ok = verify_webhook(
raw, request.headers.get("x-mailgent-signature"),
os.environ["MAILGENT_WEBHOOK_SECRET"],
)
if not ok:
raise HTTPException(400, "invalid signature")
# handle event ...Retry policy on delivery: 5xx and network errors back off (1s · 4s · 16s · 64s · 300s). 4xx aborts retries.