DEV Community

Cover image for Selling a judgement to an agent
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on AI-assisted

Selling a judgement to an agent

Building a paid trust-checker, a paid certificate, and a buyer that verifies both

A technical walkthrough of x402-trust-merchant.


Every agent demo shows the same thing: an agent finds an API and pays for it. It is
impressive for about ninety seconds, and then someone in the room asks the only question
that matters — "why would I let it pay for that?"

Because price is not the hard part any more. A 402 with a machine-readable amount is
twenty lines of code. What is missing is the thing a buyer needs before it signs, and
the thing it needs after the bytes arrive:

  • before: is this seller the thing it claims to be, and does it publish enough to be buyable?
  • after: can I prove to a third party what I received, without asking the seller to still exist?

x402-trust-merchant is a working answer to both, in about 1,400 lines of JavaScript with one
dependency. It is the safer of our two demo variants: the agent buys a commodity from one
stranger, and then buys the trust check and the result from us.

01 DISCOVER  8 candidates for "timezone"                       bazaar
02 SELECT    geospatial.openverbs.com  Resolve the timezone… $0.0020
03 PAY       geospatial.openverbs.com · $0.0020 · settled      230ms
   └ 402 header · x402 v2 · 1 offer(s) · also MPP rail "evm"
04 RESULT    Asia/Kolkata +05:30
05 DISCOVER  trust check — served by us, not the bazaar        owned
06 SELECT    our checker · target geospatial.openverbs.com     $0.0020
07 PAY       our checker · $0.0020 · settled                   640ms
08 RESULT    verdict: caution · 1/3 · 3 probes timed
09 CHECK     ERC-8004 identity #1 on base-sepolia              0x21fd…e235
10 PAY       our seller · $0.050 · settled                     18ms
11 RESULT    certificate 9c1f… · buyer 0x21fd…e235
12 ATTEMPT   agent402.tools wants $0.030 for one image         over policy
13 REFUSE    price above per-call cap · 30000 > 20000          stopped
TOTAL SPENT  $0.054   ·  2 sellers touched  ·  2.2s
Enter fullscreen mode Exit fullscreen mode

One external dependency. Two seconds. And still a real 402 from a server nobody in the
room owns, which is what makes the demo credible.


Architecture

                      ┌────────────────────────────────┐
                      │            run.mjs             │
                      │  goal → find → check → pay     │
                      │       → verify → receipt       │
                      └──────┬─────────────────┬───────┘
                             │                 │
                             ▼                 ▼
                  ┌────────────────────┐   ┌───────────────────────┐
                  │ lib/discovery.mjs  │   │ lib/guard.mjs         │
                  │ Bazaar search      │   │ Budget.approve()      │
                  └─────────┬──────────┘   │ ./STOP kill switch    │
                            │              └──────────┬────────────┘
                            ▼                         │ every signature
                  ┌─────────────────────┐             │
                  │    lib/x402.mjs     │◄────────────┘
                  │ readChallenge       │
                  │ chooseAccept · sign │
                  └────┬───────────┬────┘
                       │           │
          ┌────────────┘           └──────────────┐
          ▼                                       ▼
┌──────────────────────┐        ┌───────────────────────────────────────┐
│ stranger's 402        │        │ seller/server.mjs  (:8787)           │
│ geospatial.openverbs  │        │ /llms.txt                             │
│ (only external dep)   │        │ /.well-known/x402 · schema · agent    │
└──────────────────────┘        │ /check/endpoint   2000 μ$  ← ours     │
                                │ /buy/certificate 50000 μ$  ← ours     │
                                └──────────────┬────────────────────────┘
                                               ▼
                                ┌───────────────────────────────────────┐
                                │ checkEndpoint(): 3 probes, in parallel│
                                │ certificate.mjs: ed25519, offline     │
                                └───────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The product is a judgement, and it has to be real

A trust check that returns a constant is theatre, and a room full of engineers will spot it
in one second. So /check/endpoint?target=host does actual work: it fetches the three
documents a buyable service is supposed to publish, times each one, and scores what came
back.

{ "target": "geospatial.openverbs.com", "verdict": "caution", "score": 1,
  "evidence": [
    { "probe": "llms.txt",                 "status": 404, "ms": 118, "present": false },
    { "probe": ".well-known/x402",         "status": 200, "ms": 141, "present": true  },
    { "probe": ".well-known/agent-card.json","status": 404, "ms": 122, "present": false }
  ] }
Enter fullscreen mode Exit fullscreen mode

score 3 → clean, 1–2 → caution, 0 → unknown. Three documents, three timings,
one verdict, two thousand micro-dollars.

Two engineering details that only matter in front of an audience:

The probes are concurrent. Three sequential four-second timeouts is twelve seconds of
dead air, and nobody watches that on a stage.

const [llms, x402, card] = await Promise.all([
  probe(`${base}/llms.txt`),
  probe(`${base}/.well-known/x402`),
  probe(`${base}/.well-known/agent-card.json`),
]);
Enter fullscreen mode Exit fullscreen mode

The scheme is chosen from the target. Our first version hardcoded https://, which
meant the checker scored our own local seller 0/3 — a bug the contract test caught, and
one that would otherwise have been discovered live. localhost, 127.*, 0.0.0.0 and
[::1] get http, everything else gets https.

That bug is the case for the whole testing section below: the checker is the product, so
its contract is worth asserting.


Publish what you expect others to publish

If we sell trust checks on other people's discovery documents, our own shop has to serve
them. So the seller exposes:

Route Purpose
/llms.txt the human-and-agent front door: what we sell, what it costs
/.well-known/x402 machine-readable price list, both paid resources
/.well-known/schema.json typed input and output, versioned 1.0.0
/.well-known/agent-card.json identity, skills, contact
/check/endpoint 2000 μUSDC — the verdict
/buy/certificate 50000 μUSDC — the signed result
{ "x402Version": 2,
  "resources": [
    { "resource": "http://localhost:8787/buy/certificate", "price": "50000" },
    { "resource": "http://localhost:8787/check/endpoint",  "price": "2000"  } ] }
Enter fullscreen mode Exit fullscreen mode

A buyer that can read that document knows what we sell, what it costs and which chain it
settles on, without loading a pricing page. That is the whole difference between "an API"
and "an API an agent can buy".


The money path, briefly

The buyer signs an EIP-3009 TransferWithAuthorization, sends it, and gets bytes plus a
settlement header. Two implementation notes that took real debugging:

The header name is not settled. v1 sellers read X-PAYMENT, v2 read
PAYMENT-SIGNATURE, MPP reads Authorization: Payment. One live seller answers a request
that offers both x402 and MPP with:

400 ambiguous_payment: provide either x402 (PAYMENT-SIGNATURE) or MPP (Authorization), not both.
Enter fullscreen mode Exit fullscreen mode

So the client negotiates instead of guessing:

const attempts = [
  { 'payment-signature': header, 'x-payment': header },
  { 'payment-signature': header },
];
if (challenge.mpp) attempts.push({ authorization: `Payment ${header}` });
Enter fullscreen mode Exit fullscreen mode

The authorisation is single-use. usedNonces makes a replayed credential fail with
authorisation already used instead of minting a second certificate — and the contract
test asserts the status and the reason.


Gate the spend before you sign

if (BigInt(amount) > this.limits.maxPerCall) {
  throw new Refused('price above per-call cap', `${amount} > ${this.limits.maxPerCall}`);
}
Enter fullscreen mode Exit fullscreen mode

Refused carries a .reason and a .detail, so the tape prints a sentence instead of a
stack trace:

12 ATTEMPT   agent402.tools wants $0.030 for one image   over policy
13 REFUSE    price above per-call cap · 30000 > 20000    stopped
Enter fullscreen mode Exit fullscreen mode

The cap belongs to the buyer, not the seller. A service that can charge anything will
eventually charge everything; the only durable protection is a client-side gate in front of
the signing function, plus touch STOP for the moment you want everything to stop.


Verify the result offline

The certificate is canonical JSON, hashed, and signed with ed25519:

const { certificate, publicKey } = mint({ buyer: account.address, item, result });
const verification = check(certificate, publicKey);   // → { valid: true, digest }
Enter fullscreen mode Exit fullscreen mode

The buyer never calls back. A receipt that requires the seller to still be online is a
promise, not a proof.


Three test suites, no funded wallet required

node test/e2e.mjs        # 12 checks — spawns a seller on :8899, ephemeral wallet
node test/contract.mjs   # 30 checks — the refusal matrix + the checker's contract
node test/offline.mjs    #  3 checks — fixtures mode with fetch replaced by a landmine
Enter fullscreen mode Exit fullscreen mode

The extra three checks in this variant are the ones that matter here: /check/endpoint is a paid call, its verdict carries three timed evidence entries, and pointed at our own seller it scores clean. The last one is what found the https-only bug.

test/offline.mjs deserves a note. It replaces globalThis.fetch with a landmine that throws, then runs --fixtures. If the backup demo ever needs the network, the test fails instead of the stage.


Next

Weighted scoring (a service with a valid price list but no llms.txt is more buyable than one with neither), deeper probes, verdict caching with freshness, refunds for post-payment failure, real facilitator settlement, and exposing find / check / pay as MCP tools so a model can drive the same engine under the same budget guard.

But the smallest useful thing is the one everyone can do: publish the four documents. llms.txt, /.well-known/x402, a typed schema, an agent card. An agent that has to guess what you sell is an agent that will buy from someone else.


Run node run.mjs --fixtures for the whole tape offline, then node test/contract.mjs —
the failure states are the interesting part.

Trust Merchant Demo

Code & more: https://www.dailybuild.xyz/project/302-x402-trust-merchant

Top comments (0)