Six steps, no clone, no editor. Steps 1 and 2 need no key and spend nothing; the first thing that costs money is step 5, and it costs one cent of testnet USDC.
npx @0x402/cli underwrite 0xDB08Ce217Ce842b06baf76a0Bbb2C10f47fF9eB8
It reads the token's bytecode over RPC, asks the reference auditor for its 402 quote, runs it past the budget gate, and stops — a dry run is the default, and without a key it is the only thing that can happen. The last line names the next command.
npx @0x402/cli init
Generates a burner key into ~/.acu/config.json (0600, inside a
0700 directory) and prints the address. It is a burner: fund it with testnet
USDC and nothing else.
npx @0x402/cli status
Take the address init printed to the Circle faucet at
faucet.circle.com
and ask for Base Sepolia USDC. status reports key, balance, auditor, budget
and ledger, and ends with one arrow line: it flips from Fund … to
Ready: acu underwrite 0xDB08…. If you jump the gun,
INSUFFICIENT_USDC repeats the address and the faucet URL.
npx @0x402/cli underwrite 0xDB08Ce217Ce842b06baf76a0Bbb2C10f47fF9eB8 --live --no-settle --publish
Your agent pays the auditor over x402, verifies the seal B that comes back against all
eight checks, refuses to compose anything if one of them fails, signs seal A around it,
and publishes the body to 0G Storage. Every step prints as it happens; the verdict line
is ✓ EXECUTED or ✗ CODE with the reason indented under it.
--no-settle is not a shortcut past anything. The demo registry's
StubVerifier trusts exactly one signer, so only the reference Agent A can
list on it; a seal you sign yourself is a fully valid seal and verifies green, it just
does not go onto this registry. Ask to settle with no registry configured and
the command refuses NO_REGISTRY before it spends anything, and
--registry <address> points it at your own deployment.
The last line is Share it: …/#seal=…. That link carries the whole seal in its
fragment, which a browser never sends to a server — so the page that verifies it never
learns what it verified. Seals on chain is the same verification
run over what this registry has actually listed — every visitor's browser re-checks each
seal before the card is coloured.
claude mcp add acu -- npx -y @0x402/mcp
Zero environment variables: the server reads the same ~/.acu/config.json the
CLI wrote. Then say it in words — "underwrite 0xDB08…9eB8 at 70% LTV" — and your
agent hires the auditor, checks the seal and signs its own. That is the whole thesis; the
five steps above exist to get you here.
An Agent A is whoever needs a decision they can defend later. It hires, verifies, signs and lists; nothing about it is hosted by us, and its key never leaves its own process.
| Command | Does |
|---|---|
| acu init | Generate a burner key into ~/.acu/config.json and print the address and faucet. |
| acu status | Key, USDC balance, auditor reachability, budget left, and the single next step. --json for a machine. |
| acu quote <token> | What an audit would cost. Free, no key, never signs. |
| acu underwrite <token> | Hire, verify, seal, publish, list. Dry run unless --live. |
| acu verify <file|-> | Check a seal A or B locally. Exit 0 valid, 1 invalid. |
claude mcp add acu -- npx -y @0x402/mcp
No -e flags. Configuration resolves env > ~/.acu/config.json
> built-in default, so the environment is only ever an override:
ACU_AGENT_KEY to use a different key for one process,
ACU_AUDITOR_URL to point at an auditor that is not the reference one,
ACU_DIRECTORY_URL to trust a different directory than this site's.
Without any key at all the server still starts, and every paid tool stops at the quote.
| Tool | Does |
|---|---|
| agent_status | Can this agent underwrite right now, and what is the one next step. Call it first. |
| describe_auditor | Fetch an auditor's card from GET /agent. Free, and never trusted. |
| quote_audit | Price an audit past the budget gate. Never pays, never signs. |
| hire_audit | Pay over x402 and return a verified seal B, or the check that failed. |
| underwrite | Hire → verify → sign seal A → publish → list, refusing with a named code. |
| verify_seal | Verify a seal A or B locally, in this process. |
| get_listing | What the registry says, what the stored seal says, and whether they agree. |
import { underwrite } from "@0x402/underwriter";
const result = await underwrite(
{
token,
ltvBps: 7000,
source: null,
settle: true,
publish: true,
},
{
account, // A's key. Nothing else can sign.
agentId: "1",
auditor: { url, agentId: "2" },
resolver, // AgentIdResolver: the directory
budget, // gate, before any signature
network: "eip155:84532",
dryRun: false,
registry,
rpcUrl,
onStep: (e) => console.log(e),
},
);
// ok: { ok: true, sealA, sealHash, hire, listing }
// no: { ok: false, stage, code, detail }
No console, no process.exit, no environment: the CLI and the MCP are both
printers over this one function.
An Agent B is any x402 service willing to sign what it did. A plain x402 API is not an Agent B — it takes the money and answers with prose, and an A has nothing to embed. What makes you a B is issuing a seal.
import express from "express";
import { sealedAuditRoute } from "@0x402/auditor";
const app = express();
app.use(express.json({ limit: "1mb" }));
sealedAuditRoute(app, {
sealAccount, // your key, in your process
agentId: "2",
priceUsd: "$0.01",
payToAddress,
network: "eip155:84532",
facilitatorUrl,
sealTtlSeconds: 86_400,
og: {
network: "testnet",
apiKey,
model: undefined,
skipAttestation: false,
},
});
app.listen(4021);
That mounts two routes: GET /agent, free, the card that says who a caller
would be hiring and what you charge; and POST /audit, gated by x402 v2, which
runs the inference and returns a signed seal B. The package never reads
process.env — a misconfigured B fails at construction, not at the first
customer.
| Field | Why it is there |
|---|---|
| subject | The token this seal is about. It cannot be replayed onto another one. |
| request | A hash of the exact request answered, so an answer cannot be moved to another question. |
| inference | Model, trust mode, provider, prompt and response hashes — what actually ran. |
| teeAttestation | The TEE evidence from the 0G Compute Router: chat id, teeVerified, signed response hash. |
| findings | The reasoning, recorded. This is how a wrong answer is caught later. |
| verdict | ALLOW or DENY plus maxLtvBps — a credit limit, not a boolean. |
| expiresAt | Seals expire. A stale audit is not evidence about today's contract. |
| signature | Over the canonical encoding of every field above, so any edit is visible. |
If the inference fails, or the attestation does not come back, or the seal cannot be
signed, POST /audit answers 502 AUDIT_FAILED and no seal is
issued. No seal, no charge. The alternative — charging for an answer you could not
sign — is exactly the failure Agent A's attestation check exists to catch, and a B should
not be committing it on purpose.
Verification never crosses the network. There is no endpoint of ours that will tell you
whether a seal is good, because such an endpoint would be a thing to trust. The same
@0x402/seal runs in the CLI, in the MCP, in an Agent A, and in
the page on this site.
import { verifySealA, SealVerificationError } from "@0x402/seal";
try {
await verifySealA(seal, { expectedSubject: token, resolver, now });
// Every check below passed, including the seal B embedded inside.
} catch (err) {
if (err instanceof SealVerificationError) console.log(err.failure, err.message);
}
| Check | Refusal | What it means |
|---|---|---|
| SCHEMA | SCHEMA_INVALID | Not a seal of the shape and version claimed. |
| AGENT_ID_LIVE | AGENT_ID_NOT_LIVE | The agent id resolves to no signer in the directory. |
| SIGNATURE | SIGNATURE_INVALID · SIGNER_MISMATCH | The digest recomputed from the seal's own bytes recovers to somebody else, or to nobody. |
| SUBJECT | SUBJECT_MISMATCH | The seal is about a different token than the one asked about. |
| REQUEST | REQUEST_MISMATCH | Seal B answers a different request than the one made. |
| EXPIRY | SEAL_EXPIRED | The seal's window has closed; a fresh audit is required. |
| ATTESTATION | ATTESTATION_MISSING | An attested tier is claimed and no attestation is carried. |
| TRUST_TIER | TRUST_MODE_INSUFFICIENT | The inference ran below the tier an underwriter will act on. |
A seal A runs the first four and expiry over itself, and runs all eight over the seal B embedded in it. Verification stops at the first failure and names it, which is why the verifier marks the rows after it not reached rather than green: "we did not look" is not the same claim as "it is fine".
It proves traceability, not correctness. A perfectly signed wrong answer still verifies —
and that is the point of recording the reasoning: on InjectionUSD the model
returned DENY ten times out of ten and every time for the wrong reason, and the
only way anyone found out was that the seal wrote the reasoning down. A TEE is not
trustlessness, the seal does not cover the invoice, and holder concentration is left
null rather than invented.