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:
| Field | Meaning |
|---|---|
scheme | exact: pay this precise amount, using EIP-3009 transferWithAuthorization. |
network | base on protocol v1, eip155:8453 (CAIP-2) on protocol v2. |
maxAmountRequired | Atomic units. USDC has 6 decimals, so 14400000 is $14.40. Named amount on v2. |
asset | Base mainnet USDC, verified on chain (see below). |
payTo | Where the USDC lands. Validated as 0x plus 40 hex before the body is emitted. |
resource | Canonical URL of what is being sold. |
maxTimeoutSeconds | How long the authorization stays payable. Default 60, the spec example value. |
extra | The 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.
| v1 | v2 | |
|---|---|---|
| 402 body | the JSON body itself | same object, also base64 in the PAYMENT-REQUIRED header |
| network id | slug, base | CAIP-2, eip155:8453 |
| amount field | maxAmountRequired | amount |
| resource fields | flat on each requirement | grouped in a resource object |
| client header | X-PAYMENT | PAYMENT-SIGNATURE |
| settlement header | X-PAYMENT-RESPONSE | PAYMENT-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:
| Facilitator | Base mainnet | Note |
|---|---|---|
https://x402.org/facilitator | no | Public, no account. Advertises eip155:84532 (Base Sepolia) but not eip155:8453. Good for integration tests only. |
https://facilitator.mogami.tech | yes | Returns {"x402Version":1,"scheme":"exact","network":"base"}. Free per the x402 facilitator directory. Self-hostable. |
https://api.cdp.coinbase.com/platform/v2/x402 | yes | Coinbase 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
| Parameter | Default | Status |
|---|---|---|
| Share of uplift | 15 percent | Estimate, to tune on the first pilots |
| Free tier | first $0.50/day of uplift, per machine, forever | Estimate |
| Minimum invoice | $1.00 USDC, below which nothing is billed | Estimate |
| Settlement | USDC on Base mainnet, 6 decimals | Verified 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
});
- The machine id is
sha256(hostname + "|" + profileKey), first 16 hex, prefixedei-. Deterministic: the same box re-enrolls to the same id, so we never double count a fleet. - The wallet must be
0xplus exactly 40 hex characters. - Records are appended to
data/enrollments.jsonl, one JSON object per line, as an event log.readEnrollments()andfindEnrollment(machineId)read it back, skipping any torn line rather than throwing. data/is gitignored. Enrollment stays on the operator's machine.
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
- We never take custody. The payer signs a transfer straight to
payTo. Funds never route through an account we control. - We never touch private keys. This codebase contains no signer, no keystore, no mnemonic handling, and no dependency that could produce one. Enrollment actively rejects key-shaped input.
- We never fabricate a settlement. No facilitator, no verification:
verifyPaymentreturnsvalid: falsewith the reasonno facilitator configured. Every failure path returns a reason string, never an optimistictrue. - Settlement is USDC on Base mainnet, contract
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, verified 2026-07-26 byeth_callagainsthttps://mainnet.base.org:symbol()returnsUSDC,decimals()returns 6,version()returns 2,eth_chainIdreturns0x2105(8453). - Replay is handled by the chain, not by us. EIP-3009 authorizations carry a 32 byte nonce that the token contract itself refuses to reuse, plus a
validAfter/validBeforewindow. - Amounts are computed in fixed point, never in floating point, so a price never drifts by an atomic unit.
Not implemented yet
The honest list.
- No measured baseline. The fee is defined on uplift, but nothing in the repo measures the operator's pre-router revenue. Until that exists, every quote is an upper bound and no invoice should be issued.
- No HTTP server. This module produces and checks 402 payloads, but no route in the project serves a paid resource yet. The site is static.
- No facilitator is shipped or defaulted. The operator sets
X402_FACILITATOR_URL. We do not run one. - No client side. There is no code here that signs an EIP-3009 authorization, on purpose: signing belongs to the operator's wallet, not to us.
- No CDP facilitator integration. Its endpoint requires a CDP API key and returned 401 unauthenticated, so its exact path and auth scheme are unverified. Do not use it without reading the Coinbase docs first.
- No metering or billing loop. Enrollment records intent. Nothing yet accumulates usage, closes a period, and emits the 402.
- No
uptoscheme. The x402uptoscheme fits metered billing better thanexactand is worth revisiting once metering exists.
Sources
Every shape above was copied from these, read 2026-07-26. They are also cited inline in src/payments/x402.ts.