Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

x402-gate

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.

Install

mops add x402-gate
import X402 "mo:x402-gate";

How it fits into your canister

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();
      };
    };
  };
};

What the library handles

  • 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.

What you provide

  • Your content and your pricing.
  • A threshold ECDSA key, so your canister has an address that receives the money. X402.ethAddress turns the public key into the address.
  • One HTTPS outcall per paid request, to POST /settle on a facilitator. The Coinbase facilitator is free and works out of the box on Base.

Non custodial by design

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.

Getting the integration right

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.

Settle it yourself, no facilitator

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 it

There 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, keccak256 of 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.

A note on volume

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.

Deciding who pays

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.

Prefer not to run it yourself

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.

License

MIT.

Built by Mingle Berry Media. Follow along on X.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages