
vet402
An agent pays an x402 seller only while vet402's draft ENSIP-29 proof in its ENSv2 name holds.
Demo
About
I made vet402 put its proof inside the seller's own ENSv2 name. It buys from an x402 seller, checks what arrives, and signs the seller's offer record following the draft ENSIP-29. (x402 lets an API ask for payment with HTTP 402, so an agent can pay per call in USDC.) An agent checks that proof before it signs. With vet402's API cut off, it still paid (Base Sepolia, 2026-09-26 00:29 UTC). After the seller changed one character of its offer (Sepolia block 11782719), the same command refused before signing. vet402 and @vet402/sdk existed before the event. Here I built the ENSv2 path, a Base Sepolia profile, the /tokyo page and the demo. vet402 buys from x402 sellers to check their promises. In the 30 days to 2026-09-16 15:00 UTC, 426 of 1,008 comparable purchases did not match the declaration (42.3%). In the 30 days to 2026-09-26 it was 533 of 1,349 (39.5%). So the proof goes in the seller's name, where an agent can check it before it signs. The whole promise is one text record, x402-offer, with price, payTo and URL in one line. The attestation sits next to it. On 2026-09-25 vet402 did this for four sellers, and all four verify as VALID 7/7. This depends on ENSv2: Enhanced Access Control works per record key. The seller's server key can rewrite its price, but writing the proof next to it with that key reverts with EACUnauthorizedAccountRoles (eth_call, Sepolia block 11782193). The attestation cannot be forged anyway. The split adds least privilege. A leaked server key can change the price, and every agent then refuses, but it cannot restore an older offer together with the attestation that signed it. The paying agent is also a name, agent-1.vet402.eth, with its own resolver and policy (trust atst.vet402.eth, at most 0.05 USDC). Then I tried to break it. - With vet402 unreachable, the agent asked, got no answer, and paid, because the proof met its declared floor. - One character changed: refused at step 7 (ens_attestation_signer_mismatch) without asking vet402. Try it on seller-d.eth at https://vet402.com/tokyo. - Borrowing another seller's attested record with linkToRecord was refused, because the name is inside the proof (block 11782107). - With the agent's name deleted, the parent's wildcard answered instead, and the agent refused (agent_policy_missing) because the answer was not from its own resolver (block 11782115). - Intercepta screens first, every time. seller-e.eth's payee is sanctioned, so nothing is paid. seller-a.eth's payee has no history on Base, so the attester holds it for a person (block 11783180), and the agent pays it only at 0.01 USDC or less with a VALID proof. Two limits remain. vet402 is the only attester, so docs/tokyo-2026/attester-spec.md states what a second one must do. Every ENS name here is mine. The independent evidence is a real Base mainnet seller vet402 bought from unasked (2026-09-17 12:09 UTC, block 51428826), logged under obs.vet402.eth (block 11782167). This stays in payOrRefuse: it takes a name only through this check, and its payee field still rejects names. The agent does not need my server to answer. It checks my signature in the seller's name, and its own file decides which attesters it trusts. Requirements, every tx and a one-line check: https://github.com/kzmttkc/vet402/blob/tokyo-2026-submission/docs/tokyo-2026/for-reviewers.md AI use: AI_USAGE.md (Tokyo), docs/tokyo-2026/human-log.md, docs/tokyo-2026/PROMPTS/. Disclosure sent to ETHGlobal 2026-09-18 UTC, confirmed complete 2026-09-24.
How it's made
vet402 is a TypeScript monorepo: a Next.js app on Vercel (the API and the /tokyo page) and @vet402/sdk, the client an agent calls before it pays. The SDK has no runtime dependencies, and viem is an optional peer. The ENSv2 code loads through a dynamic import only when a caller passes a name, so callers that pay by address never load it. The ENSIP-29 codec carries its own small DAG-CBOR encoder. Its test compares the bytes with a fixture made once with @ipld/dag-cbor 9.2.6, which is not a dependency. Every ENS read pins one Sepolia block, the lower of two heads, on two independent public RPCs, and a value counts only when both return the same bytes. The current manager comes from UniversalHelper.findExactOwner. The offer and the attestation come through UniversalResolver.resolve, so linked records apply. That is why borrowing another name's record fails. The trust list is the consumer's own file of (name, address) pairs, with each address pinned locally and checked against the name's current addr. The public verifier accepts attestations up to 7 days old, while the demo agent's on-chain policy accepts 24 hours. That policy is read only from the agent's own resolver. An answer that reaches it through the parent's wildcard is refused. The attester buys before it signs, in six steps (docs/tokyo-2026/attester-spec.md). Payments are x402 on Base Sepolia in test USDC. In the demo agent, Intercepta screening runs before anything else. In the attester, it runs before anything is bought. No answer within 3 seconds, a 404 or an unexpected body means no payment. If vet402's API is unreachable (no connection, or HTTP 5xx), the caller's declared floor decides, and the decision line records vet402_unreachable and allowed_by_caller_policy. A 4xx, a degraded answer or a BLOCK that does arrive is never waived. The demo installs the SDK from a packed tarball with viem fixed at 2.55.1, so a clean clone runs on its own. Its --live commands check the chainId, simulate the transaction alone, check the signer's balance, and send nothing until a person types y. The first failing tests went in at b9d6fe74 (2026-09-25 12:08 UTC), before the code that makes them pass.