Pay per crawl for Internet Computer canisters. Charge AI crawlers for your content and settle in USDC, without giving up custody of the money.
AI bots already make up more than half of the traffic to a lot of sites, and most of them read your content for free. This library lets your canister ask them to pay first. It speaks the x402 standard (the HTTP 402 flow that Coinbase, Cloudflare, Stripe and others have settled on), so any agent that already pays for x402 resources can pay yours too.
The receiving address belongs to your canister through threshold ECDSA. The payment goes straight from the agent to that address. You are never in the position of holding someone else's funds, and neither is anyone else.
mops add x402-gate
import X402 "mo:x402-gate";You already serve content from http_request. You add three things in front of it: return a 402 with your price when there is no payment, verify the payment inside the canister, then settle it and serve.
let cfg : X402.Config = {
network = "base-sepolia"; // or "base"
chainId = 84532;
asset = "0x036cbd53842c5426634e7929541ec2318f3dcf7e"; // USDC on Base Sepolia
assetName = "USDC";
assetVersion = "2";
payTo = myAddress; // your threshold ECDSA address
};
let reqJson = X402.requirementsJson(cfg, resourceUrl, "My article", "text/html", 10_000);
switch (getHeader(req, "x-payment")) {
case null {
// No payment yet. Tell the agent what it costs.
return respJson(402, X402.errorBody("payment required", reqJson));
};
case (?header) {
let payload = switch (X402.b64Decode(header)) { case (?b) decodeUtf8(b); case null return respJson(402, X402.errorBody("bad header", reqJson)) };
switch (X402.verifyPayment(cfg, ctx, payload, 10_000, nowSeconds)) {
case (?err) { return respJson(402, X402.errorBody(err, reqJson)) };
case null {
// Signature, amount and time window all check out.
// Settle it with one HTTPS outcall to the facilitator, then serve.
let settleBody = X402.settleRequestBody(payload, reqJson);
// ... post settleBody to the facilitator, check success ...
return serveContent();
};
};
};
};- The x402 payment requirements as JSON, ready to drop into your 402 response.
- Verifying the payment signature (EIP-712 / EIP-3009) right inside the canister. This costs no cycles, so a forged or underpaid request is rejected before you ever make an outcall.
- Building the ERC-20 transfer calldata for withdrawals.
- Deriving your Base address from a threshold ECDSA public key.
- The small stuff you would otherwise write by hand: base64, hex, keccak256, address recovery.
- Your content and your pricing.
- A threshold ECDSA key, so your canister has an address that receives the money.
X402.ethAddressturns the public key into the address. - One HTTPS outcall per paid request, to
POST /settleon a facilitator. The Coinbase facilitator is free and works out of the box on Base.
The address in payTo is controlled only by your canister's threshold signature. The library never holds funds and never asks you to route money through a third party. Payments settle directly between the agent and your wallet.
Two rules keep the paywall safe. Both live on your side, because the library only checks a single payment in isolation.
Settle before you serve. A passing verifyPayment means the signature and the amount look right, not that the money moved. Serve the content only after the facilitator confirms the settlement. Verify and serve on its own is not safe.
Deduplicate by nonce. The same signed payment can be sent again. Keep a small store of nonces you have already settled and either reject a repeat or serve the cached response without settling a second time. Use the nonce for this, never the signature, since a signature can be reshaped without changing the signer.
Use 0.1.1 or newer. Version 0.1.0 has a value range bug that lets a crafted payment pass for nothing. See the changelog.
You do not need a facilitator at all. Your canister already has a threshold key, so it can run the payment on chain itself. That keeps everything on the Internet Computer with no third party in the settlement path.
After verifyPayment passes, build the calldata and send it:
let calldata = X402.settleCalldata(payload); // transferWithAuthorization, ready to sign
// build an EIP-1559 tx: to = the USDC contract, data = calldata, value = 0
// sign it with your threshold key, then broadcast it with eth_sendRawTransaction
// your address pays the gas, so keep a little ETH on itThere is one thing about the Internet Computer you have to get right, or you will see failures on payments that actually went through. An HTTPS outcall runs on every replica in the subnet, so your eth_sendRawTransaction is sent many times over. The first one lands, the rest come back with "already known", and consensus can settle on that error even though the transaction is already on chain. So do not read the broadcast response as the truth:
- Compute the transaction hash yourself,
keccak256of the signed raw bytes, instead of taking it from the response. - In the transform function for the broadcast, return an empty body, so every replica returns the same thing and consensus is reached.
- Then read the receipt for the real outcome. Reduce that response to the status field alone in its transform, and be ready to retry, because a fresh transaction looks different to different replicas for a moment. Treat a moment of disagreement as "not yet", not as failure.
Serve the content only once the receipt shows success.
At high crawl volume the one outcall per payment starts to matter for cycles. If you expect a lot of traffic, batch several payments into a single settlement. The x402 batch schemes are built for exactly this.
x402-gate charges AI crawlers to read. Writing is the harder question: a real person posting should not pay, only one operator hiding behind many accounts should. sybil-signals is the companion for that side. It reads behaviour alone, no identity check and no payment, and returns signals and a score without passing judgment, so your canister decides who needs a gate at all. It composes with x402-gate, with ic402, or with a direct ICRC-2 call.
If you would rather not wire up the threshold key, the outcalls and the hosting, there is a managed version. It sets up the gate, keeps it funded, and gives you a dashboard. See gate.mingleberrymedia.com.
MIT.
Built by Mingle Berry Media. Follow along on X.