Payments

Effective Intelligence is sold agent to agent. The operator's AI agent enrolls a machine, reads the plan, and pays for the service itself, over x402: HTTP 402 plus a gasless USDC transfer on Base. No signup form, no dashboard, no card, no invoice email.

Everything below is implemented in src/payments/, verified by bun run src/payments/smoke.ts.

The 402 handshake, step by step

The router is the resource server. The operator's agent is the client. A facilitator does the chain work.

1. The agent calls a paid resource with no payment attached.


GET /api/plan HTTP/1.1
Host: effective-intelligence-app.netlify.app

2. The server answers 402 with machine-readable payment requirements.

buildPaymentRequirements({ priceUsdc, payTo, resource, description }) returns exactly this body:


{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "14400000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x...",
      "resource": "https://effective-intelligence-app.netlify.app/api/plan",
      "description": "Effective Intelligence: 30 days of yield routing for one machine",
      "mimeType": "application/json",
      "outputSchema": null,
      "maxTimeoutSeconds": 60,
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

Field by field:

FieldMeaning
schemeexact: pay this precise amount, using EIP-3009 transferWithAuthorization.
networkbase on protocol v1, eip155:8453 (CAIP-2) on protocol v2.
maxAmountRequiredAtomic units. USDC has 6 decimals, so 14400000 is $14.40. Named amount on v2.
assetBase mainnet USDC, verified on chain (see below).
payToWhere the USDC lands. Validated as 0x plus 40 hex before the body is emitted.
resourceCanonical URL of what is being sold.
maxTimeoutSecondsHow long the authorization stays payable. Default 60, the spec example value.
extraThe EIP-712 domain of the token, so the agent can sign. name is USD Coin on Base mainnet and USDC on Base Sepolia. Signing against the wrong domain fails verification.

3. The agent signs an EIP-3009 authorization and retries.

It never sends a transaction. It signs an off-chain EIP-712 message authorizing a transfer of exactly maxAmountRequired USDC from its own wallet to payTo, valid for a short window, with a random 32 byte nonce. That signature plus the authorization is base64-encoded into a header:


GET /api/plan HTTP/1.1
X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZSIsInBheWxvYWQiOnsic2lnbmF0dXJlIjoiMHguLi4i...

Decoded, that header is:


{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base",
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0x<the agent wallet>",
      "to": "0x<payTo>",
      "value": "14400000",
      "validAfter": "1740672089",
      "validBefore": "1740672154",
      "nonce": "0x<32 random bytes>"
    }
  }
}

4. The server verifies.

verifyPayment(header, requirements) first runs local checks that need no network: the payload decodes, the protocol version matches, the scheme and network match, the recipient matches payTo, and the value matches to the atomic unit. Then it POSTs to the facilitator:


POST {X402_FACILITATOR_URL}/verify
{ "x402Version": 1, "paymentPayload": { ... }, "paymentRequirements": { ... } }

and maps the answer { isValid, invalidReason?, payer? } to { valid, payer, reason }. The facilitator is the one checking the signature, the balance, the time window, and simulating the transfer.

5. The server settles, then serves the resource.

settlePayment(header, requirements) POSTs the same body to {facilitator}/settle. The facilitator broadcasts transferWithAuthorization and returns { success, transaction, network, payer }. The server then returns 200 with the resource and echoes the settlement in X-PAYMENT-RESPONSE, base64-encoded.

Protocol versions

Both are implemented. Pass x402Version: 2 to buildPaymentRequirements for the newer shape.

v1v2
402 bodythe JSON body itselfsame object, also base64 in the PAYMENT-REQUIRED header
network idslug, baseCAIP-2, eip155:8453
amount fieldmaxAmountRequiredamount
resource fieldsflat on each requirementgrouped in a resource object
client headerX-PAYMENTPAYMENT-SIGNATURE
settlement headerX-PAYMENT-RESPONSEPAYMENT-RESPONSE

Default is v1, because the facilitator that covers Base mainnet today speaks v1 only.

Configuring a facilitator


export X402_FACILITATOR_URL=https://facilitator.mogami.tech
bun run src/payments/smoke.ts

There is no default facilitator. Probed on 2026-07-26 with GET /supported:

FacilitatorBase mainnetNote
https://x402.org/facilitatornoPublic, no account. Advertises eip155:84532 (Base Sepolia) but not eip155:8453. Good for integration tests only.
https://facilitator.mogami.techyesReturns {"x402Version":1,"scheme":"exact","network":"base"}. Free per the x402 facilitator directory. Self-hostable.
https://api.cdp.coinbase.com/platform/v2/x402yesCoinbase Developer Platform. Returns 401 without a CDP API key, so its capability list is unverified here.

Defaulting to the public one would mean testnet settlements reported as real payments. So when nothing is set, verifyPayment returns:


{ "valid": false, "reason": "no facilitator configured" }

That is the whole point: an unverifiable payment is a failed payment, never an assumed one.

The fee model

src/payments/pricing.ts, pure functions, no IO.

We charge a share of the measured yield uplift we create, never a share of gross revenue and never a flat rent. If the router does not beat what the operator already ran, the bill is zero.


uplift/day     = max(0, plan total $/day  -  measured baseline $/day)
billable       = (uplift/day - free tier) x days
fee            = billable x 15 percent
ParameterDefaultStatus
Share of uplift15 percentEstimate, to tune on the first pilots
Free tierfirst $0.50/day of uplift, per machine, foreverEstimate
Minimum invoice$1.00 USDC, below which nothing is billedEstimate
SettlementUSDC on Base mainnet, 6 decimalsVerified on chain

Worked example, the reference machine in the README (RTX 4090 at $0.10/kWh, plan total $3.70/day) over 30 days with no measured baseline:


uplift 3.70/day  ->  free tier waives 0.50/day  ->  3.20/day billable
3.20 x 30 = 96.00 billable uplift  ->  x 0.15  =  14.40 USDC  =  14400000 atomic

With a real baseline the bill collapses to what it should be. The same machine already earning $2.90/day on Vast has an uplift of $0.80/day, of which $0.50 is free, so 30 days cost $1.35 USDC, not $14.40.

quoteUsdcForPlan(planTotalUsdDay, days) without a baseline returns measured: false and flags the quote as an upper bound, not an invoice. A real invoice requires a measured baseline over the same window. That measurement is the honest hard part of the product, and it is not built yet.

describePricing() returns the same model machine-readable, for an agent that wants to reason about cost before enrolling.

Enrollment

src/payments/enroll.ts. Fully local: no network call, no signature, no key.


import { enrollMachine } from "./src/payments";

const record = enrollMachine({
  wallet: "0x0000000000000000000000000000000000000001", // public address only
  hardwareProfileKey: "rtx-4090",
  plan,                                                 // from src/plan.ts
});

Anything that looks like signing material is refused with a SecretMaterialError, before a single byte hits disk: 64 hex characters with or without 0x, a PEM private key block, an xprv/yprv/zprv/tprv extended key, a 12 to 24 word BIP-39 phrase, or any field mentioning a private key or a seed phrase. The check runs on every string field, not just the wallet.

Security stance

Not implemented yet

The honest list.

Sources

Every shape above was copied from these, read 2026-07-26. They are also cited inline in src/payments/x402.ts.