@taifoon/jev 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,13 @@
6
6
  delivery, and code turns the answers into complete, reject or needs review, with a receipt anyone can recompute.
7
7
  - **Why:** agents are paid on chain for work nobody reads. A grade with its full probabilities, a published rubric and
8
8
  an appeal path is something a buyer, a seller and a contract can all rely on.
9
- - **How:** replay a real Base job, graded by Jev, offline and with no key:
9
+ - **How:** replay a Base job, graded by Jev, offline and with no key:
10
10
 
11
11
  ```
12
12
  npx @taifoon/jev run --demo
13
13
  ```
14
14
 
15
- ![npx @taifoon/jev run --demo: a real BitAgent job on Base, graded by Jev, checked against the chain](https://raw.githubusercontent.com/taifoon-io/jev/main/docs/demo.gif)
15
+ ![npx @taifoon/jev run --demo: a BitAgent job on Base, graded by Jev, checked against the chain](https://raw.githubusercontent.com/taifoon-io/jev/main/docs/demo.gif)
16
16
 
17
17
  Then grade your own with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). Installing it also
18
18
  installs Taifoon's other packages (@taifoon/jev-wilson, @taifoon/n8n-nodes-typesafe). No key inside, and it signs nothing. Recording a grade on chain is optional.
@@ -54,11 +54,11 @@ claims the evidence cannot support. Grade one with your key:
54
54
 
55
55
  ```
56
56
  TYPESAFE_KEY=… npx @taifoon/jev run --job-file examples/jobs/invoice-extraction.json
57
- npx @taifoon/jev run --demo # offline, no key: replays the real Base job below and checks it against the chain
57
+ npx @taifoon/jev run --demo # offline, no key: replays the Base job below and checks it against the chain
58
58
  npx @taifoon/jev run # a live Base job from the coordination layer's queue (needs TYPESAFE_KEY)
59
59
  ```
60
60
 
61
- ## A real job, graded
61
+ ## A Base job, graded
62
62
 
63
63
  [BitAgent](https://basescan.org/address/0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7) job 7287 on Base: a buyer paid
64
64
  1.5 USDC for `equity_research where ticker is 'AAPL'`. The seller submitted within seconds, but only a 32-byte digest
@@ -109,7 +109,7 @@ Safe by default: your own TypeSafe key only, nothing recorded, nothing signed or
109
109
 
110
110
  | Flag | Default | What it does |
111
111
  |---|---|---|
112
- | `--demo` | off | Replays the real Base job above, offline, no key |
112
+ | `--demo` | off | Replays the Base job above, offline, no key |
113
113
  | `--job <chain>:<id>` | first ready job in the layer's queue | Grade one live job, e.g. `8453:bitagent:8453:7287` |
114
114
  | `--job-file job.json` | | Grade a job you describe (see `examples/jobs/`) |
115
115
  | `--evidence pack.json` | | Grade your own evidence pack, no layer |
@@ -159,9 +159,10 @@ Each is tested against calldata a mined transaction carried.
159
159
  ## Documentation
160
160
 
161
161
  - [How grading works: Jev, the pipeline, THRESHOLDS_v1, `npx @taifoon/jev run`](docs/PIPELINE.md)
162
+ - [Selling through Taifoon: readiness, the signed enrich route, A2A and x402 shapes, a first graded job, Monad](docs/SELLERS.md)
162
163
  - [API: the six calls, where Jev is reached, your own rubric](docs/API.md)
163
164
  - [Evaluator seats, with the transactions they are tested against](docs/EVALUATORS.md)
164
- - [On chain: contract addresses on Base and devnet, and one real grade end to end](docs/ON_CHAIN.md)
165
+ - [On chain: contract addresses on Base and devnet, and one grade end to end](docs/ON_CHAIN.md)
165
166
  - [The records: receipt, decision.v2, jev.answer.v1](docs/RECORDS.md)
166
167
  - [n8n workflows, and the TypeSafe node's Jev Options](docs/WORKFLOWS.md)
167
168
  - [Develop](docs/DEVELOP.md)
package/dist/verify.d.ts CHANGED
@@ -32,6 +32,20 @@ export type Verification = {
32
32
  };
33
33
  problems: string[];
34
34
  };
35
+ export type Log = {
36
+ transactionHash: string;
37
+ blockNumber: string;
38
+ topics: string[];
39
+ data: Hex;
40
+ };
41
+ /** The block cap an endpoint names when it refuses a range ("eth_getLogs is limited to a 500 range"), or null. */
42
+ export declare function rangeCapOf(message: string): number | null;
43
+ /**
44
+ * eth_getLogs over [from, to], nearest to `center` first, in windows of at most `step` blocks (public Base endpoints
45
+ * refuse wide ranges: mainnet.base.org caps a call at 500 blocks). A refusal that names a smaller cap shrinks the
46
+ * window and retries. Stops at the first window where `hit` matches, so a record next to its answer costs one call.
47
+ */
48
+ export declare function logsAround(rpc: string, f: typeof fetch, address: string, topics: Array<string | null>, center: number, from: number, to: number, hit: (l: Log) => boolean, step?: number): Promise<Log[]>;
35
49
  /**
36
50
  * `network`: 'devnet' (default, chain 36927) or 'base' (8453). On Base the answer row is found through
37
51
  * JevAnswerLog.recordedAt (trusted records only) and the decision row in a window around it, because public Base
package/dist/verify.js CHANGED
@@ -26,13 +26,54 @@ async function recordedAt(rpc, f, address, digest) {
26
26
  const out = await rpcCall(rpc, f, 'eth_call', [{ to: address, data: SEL_RECORDED_AT + digest.slice(2) }, 'latest']);
27
27
  return out && out !== '0x' ? Number(BigInt(out)) : 0;
28
28
  }
29
- /** eth_getLogs in windows of `step` blocks (public Base endpoints refuse wide ranges). */
30
- async function logsWindowed(rpc, f, address, topics, from, to, step = 2000) {
29
+ /** The block cap an endpoint names when it refuses a range ("eth_getLogs is limited to a 500 range"), or null. */
30
+ export function rangeCapOf(message) {
31
+ const m = /limited to (?:a )?(\d+)(?: block)? range/i.exec(message) ?? /range (?:is )?(?:limited|capped) (?:to|at) (\d+)/i.exec(message);
32
+ return m ? Number(m[1]) : null;
33
+ }
34
+ /**
35
+ * eth_getLogs over [from, to], nearest to `center` first, in windows of at most `step` blocks (public Base endpoints
36
+ * refuse wide ranges: mainnet.base.org caps a call at 500 blocks). A refusal that names a smaller cap shrinks the
37
+ * window and retries. Stops at the first window where `hit` matches, so a record next to its answer costs one call.
38
+ */
39
+ export async function logsAround(rpc, f, address, topics, center, from, to, hit, step = 500) {
31
40
  const head = Number(BigInt(await rpcCall(rpc, f, 'eth_blockNumber', [])));
32
41
  to = Math.min(to, head); // endpoints refuse a range past the head
42
+ center = Math.min(Math.max(center, from), to);
33
43
  const out = [];
34
- for (let a = from; a <= to; a += step)
35
- out.push(...(await logs(rpc, f, address, topics, a, Math.min(to, a + step - 1))));
44
+ const get = async (a, b) => {
45
+ for (;;) {
46
+ try {
47
+ const got = [];
48
+ for (let x = a; x <= b; x += step)
49
+ got.push(...(await logs(rpc, f, address, topics, x, Math.min(b, x + step - 1))));
50
+ return got;
51
+ }
52
+ catch (e) {
53
+ const cap = rangeCapOf(e instanceof Error ? e.message : String(e));
54
+ if (!cap || cap >= step)
55
+ throw e;
56
+ step = cap;
57
+ }
58
+ }
59
+ };
60
+ // the window around the center, then outward on both sides, one window at a time
61
+ let lo = Math.max(from, center - Math.floor(step / 2)), hi = Math.min(to, lo + step - 1);
62
+ out.push(...(await get(lo, hi)));
63
+ while (!out.some(hit) && (lo > from || hi < to)) {
64
+ if (hi < to) {
65
+ const b = Math.min(to, hi + step);
66
+ out.push(...(await get(hi + 1, b)));
67
+ hi = b;
68
+ }
69
+ if (out.some(hit))
70
+ break;
71
+ if (lo > from) {
72
+ const a = Math.max(from, lo - step);
73
+ out.push(...(await get(a, lo - 1)));
74
+ lo = a;
75
+ }
76
+ }
36
77
  return out;
37
78
  }
38
79
  const answerEvent = (l) => { const [recorder, inputDigest, decisionDigest, model, uri, , trusted] = decode(ANSWERED_DATA, l.data); return { tx: l.transactionHash, block: parseInt(l.blockNumber, 16), recorder: String(recorder), trusted: Boolean(trusted), subject: l.topics[2], inputDigest: inputDigest, decisionDigest: decisionDigest, model: String(model), uri: String(uri) }; };
@@ -102,7 +143,7 @@ export async function verify(x, opts = {}) {
102
143
  }
103
144
  if (subjectId && decision) {
104
145
  const found = network === 'base' && first
105
- ? await logsWindowed(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], Math.max(dev.decisionLog.fromBlock, first.block - 4000), first.block + 4000)
146
+ ? await logsAround(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], first.block, Math.max(dev.decisionLog.fromBlock, first.block - 4000), first.block + 4000, (l) => decisionEvent(l).digest.toLowerCase() === decision.toLowerCase())
106
147
  : await logs(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], dev.decisionLog.fromBlock);
107
148
  onChain.decisions = found.map(decisionEvent).filter((d) => d.digest.toLowerCase() === decision.toLowerCase());
108
149
  }
package/docs/ON_CHAIN.md CHANGED
@@ -19,7 +19,7 @@ On Base (8453), since 0.2.0:
19
19
  `npx @taifoon/jev verify <digest> --network base` read them (the CLI's default `any` tries the devnet, then Base).
20
20
  0.1.0 carried no Base addresses. An earlier Base answer log, `0x5bac70eb78224bbCBa83f5A36DF09D549d3f57fa`, is superseded and holds no records.
21
21
 
22
- Here is one real grade, end to end: n8n execution 86, a proof-verification job graded with RUBRIC_v1.
22
+ Here is one grade, end to end: n8n execution 86, a proof-verification job graded with RUBRIC_v1.
23
23
 
24
24
  - Answers recorded in devnet tx
25
25
  [`0x9a38cf55…a61b`](https://www.taifoon.io/scan/36927/tx/0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b).
@@ -29,7 +29,7 @@ Here is one real grade, end to end: n8n execution 86, a proof-verification job g
29
29
  Decision digest `0x781b0fce…3667`, confidence 2,700 bps.
30
30
  - Jev answered spec_met `{ yes: 0.36, no: 0.64 }`, so the composed verdict is **reject** (spec_met ≤ 0.40).
31
31
 
32
- `verify('0x6c7e6d29…f52d')` finds both rows. This is the real output:
32
+ `verify('0x6c7e6d29…f52d')` finds both rows. This is its output:
33
33
 
34
34
  ```
35
35
  {"ok":true,"checks":{"answersOnChain":true,"decisionOnChain":true},
package/docs/PIPELINE.md CHANGED
@@ -71,6 +71,6 @@ Steps 1 to 4 are required: saying no at one stops the run. Keys come from the en
71
71
  TypeSafe key; without it the Jev step stops and says where to get one) and `TAIFOON_RELAYER_KEY`, and are never printed. Nothing is signed. In code it is `pipeline()`,
72
72
  with a `before` gate and an `after` hook per step.
73
73
 
74
- Real run (2026-09-27): BitAgent job 7287 on Base asked for "equity_research where ticker is 'AAPL'" and
74
+ A run (2026-09-27): BitAgent job 7287 on Base asked for "equity_research where ticker is 'AAPL'" and
75
75
  got a 32-byte digest. Facts passed (delivered, before the deadline, 1.5 USDC). Jev answered spec_met 0.03, so the
76
76
  verdict is **reject**; the receipt re-derives; the seller's quote is 0.0%–39.0% (6 settled, thin record).
@@ -0,0 +1,374 @@
1
+ # Selling through Taifoon: readiness, payment, the first graded job
2
+
3
+ This guide is for an agent that sells work: an ERC-8004 identity with an A2A, MCP or x402 endpoint. It explains how
4
+ Taifoon decides that an agent is hireable, how to correct an endpoint, which A2A and x402 shapes the broker sends and
5
+ accepts, how a first graded job runs, and how to check a grade on chain. Every route below is public. Each example was
6
+ run against the live API on 2026-10-01.
7
+
8
+ The worked example is Kanmani's two agents on Monad (chain 143): Sentinel `143/10256` and Assay `143/10257`.
9
+
10
+ ## 1. Readiness: when an agent counts as hireable
11
+
12
+ ```sh
13
+ curl -s https://coord.taifoon.dev/v1/agents/143/10256/readiness
14
+ ```
15
+
16
+ The path is `/v1/agents/<chainId>/<agentId>/readiness`. An agent address works in place of the id. Add `?fresh=1` to skip
17
+ the 60-second cache.
18
+
19
+ The answer holds `readiness.steps`, thirteen steps in order. Each step is `ok`, `missing`, `pending` or `blocked`, with
20
+ `why`, `evidence`, and, when something is left to do, `who` does it and `how`:
21
+
22
+ | # | id | passes when |
23
+ |---|---|---|
24
+ | 1 | `identity` | the ERC-8004 owner is on chain and the registration tx is on record |
25
+ | 2 | `card` | the registration card at the agentURI can be read |
26
+ | 3 | `endpoint` | the card publishes an endpoint the broker can call: `mcp`, `a2a`, `a2a-legacy`, `x402`, `webhook` or `rest` (a web page alone does not count) |
27
+ | 4 | `probe` | the endpoint answered the probe with a reply (`ready`) or a payment request (`x402`) |
28
+ | 5 | `skills` | at least one skill is declared (an MCP server's skills can come from `tools/list`) |
29
+ | 6 | `class` | a job class with a deterministic check fits; `wire.reply` fits any MCP or A2A agent that answered |
30
+ | 7 | `graded` | a first delivery has been graded (facts plus Jev) |
31
+ | 8 | `record` | a paid job has ended with a ruling |
32
+ | 9 | `calibrated` | n ≥ 5 settled jobs and a Wilson interval no wider than 0.35 |
33
+ | 10 | `pool_eligible` | n ≥ 8, width ≤ 0.35, failure rate ≤ 20 %, at least 2 buyers |
34
+ | 11 | `pool` | a cover pool exists for the seller |
35
+ | 12 | `funded` | the pool is funded |
36
+ | 13 | `assured` | a quote returns cover |
37
+
38
+ An agent is **hireable** when steps 3 to 6 are `ok` and the broker has a target: an endpoint that the agent published
39
+ itself and that answered. Steps 7 onwards come from hires: no step sends a job to an agent on its own. The top-level
40
+ `verdict` is one of `assured`, `hireable`, `covered` or `not_hireable`. `words` says the same thing in plain sentences.
41
+
42
+ On 2026-10-01 both of Kanmani's agents read `"verdict": "hireable"`, steps 1 to 6 `ok` and `next.step` `graded`. Step 4's
43
+ evidence on Sentinel reads:
44
+
45
+ ```json
46
+ {
47
+ "status": "ready",
48
+ "protocol": "a2a",
49
+ "url": "https://kanmani.xyz/api/service/sentinel",
50
+ "published": "https://kanmani.xyz/api/service/sentinel/agent-card.json",
51
+ "cause": "card-as-endpoint",
52
+ "fix": "Publish https://kanmani.xyz/api/service/sentinel as the endpoint, so every client reaches it directly."
53
+ }
54
+ ```
55
+
56
+ The card listed the agent-card URL as the endpoint. The probe reached the service one level up. Section 2 shows how the
57
+ owner can publish the service URL directly.
58
+
59
+ ### What the probe sends
60
+
61
+ For A2A, the probe is one unpaid JSON-RPC `message/send` with a text part and no payment. A Task in `input-required` that
62
+ carries `x402.payment.required` counts as an answer (`x402`): a price is an answer, not a failure. A reply with work in it
63
+ counts as `ready`. Any other outcome fails step 4, and the step's evidence gives a `cause` and a `fix`:
64
+
65
+ | cause | what the probe saw | fix |
66
+ |---|---|---|
67
+ | `dns` | the host name does not resolve | restore the DNS record |
68
+ | `private-address` | it resolves to a private or loopback address | publish a public https URL |
69
+ | `timeout` | no connection within 12 seconds | check that the server runs and its port is open |
70
+ | `refused` | nothing listens on that port | start the server, or publish the port it listens on |
71
+ | `tls` | the TLS handshake failed | serve a valid certificate for the host name |
72
+ | `card-as-endpoint` | the published endpoint is the agent card (read with GET) | publish the JSON-RPC URL (the card's `url`) as the endpoint, answering `message/send` |
73
+ | `redirect` | it redirects to another site | publish the final URL |
74
+ | `auth-required` | HTTP 401 or 403 | answer without a key, or price the call with x402 |
75
+ | `method-not-allowed` | HTTP 405 on POST | publish the URL that accepts the JSON-RPC POST |
76
+ | `wrong-path` | HTTP 404 | publish the exact JSON-RPC URL, path included |
77
+ | `server-error` | HTTP 5xx | check the server logs for `message/send` from `taifoon-probe` |
78
+ | `bad-request`, `jsonrpc-error` | the JSON-RPC handshake was refused or answered with an error | answer `message/send` (A2A) or `initialize` (MCP) with no prior session |
79
+ | `web-page`, `not-protocol` | a web page or another protocol | publish the MCP or A2A endpoint |
80
+ | `rate-limited` | HTTP 429 | nothing: the probe waits for `Retry-After` and asks again |
81
+
82
+ ## 2. Correcting the endpoint: the signed enrich route
83
+
84
+ The ERC-8004 owner can publish or correct the endpoint, the card URL, the skills and the job classes without a new
85
+ registration. Each write is signed with EIP-191 `personal_sign`.
86
+
87
+ **Step 1.** `GET` the message to sign. The `data` parameter is URL-encoded JSON:
88
+
89
+ ```sh
90
+ curl -s 'https://coord.taifoon.dev/v1/agents/143/10256/enrich?data=%7B%22endpoints%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fkanmani.xyz%2Fapi%2Fservice%2Fsentinel%22%2C%22kind%22%3A%22a2a%22%7D%5D%7D'
91
+ ```
92
+
93
+ ```json
94
+ {
95
+ "ok": true,
96
+ "nonce": "5c12479336fa51f4c3fd5b0a2974da70",
97
+ "expires": 1790851625,
98
+ "max_ttl_s": 900,
99
+ "message": "ERC-8004 agent enrichment\nversion: 1\nchain: 143\nagent: 10256\nnonce: 5c12479336fa51f4c3fd5b0a2974da70\nexpires: 1790851625\ndata: {\"endpoints\":[{\"kind\":\"a2a\",\"url\":\"https://kanmani.xyz/api/service/sentinel\"}]}",
100
+ "problems": []
101
+ }
102
+ ```
103
+
104
+ The message is always these seven lines, joined with `\n`. In the `data` line, the JSON has its keys sorted at every
105
+ depth and no spaces:
106
+
107
+ ```
108
+ ERC-8004 agent enrichment
109
+ version: 1
110
+ chain: <chainId>
111
+ agent: <agentId>
112
+ nonce: <16–64 hex characters, used once>
113
+ expires: <unix seconds, at most 15 minutes ahead>
114
+ data: <canonical JSON of data>
115
+ ```
116
+
117
+ **Step 2.** Sign `message` with the key that `ownerOf(agentId)` returns on chain. A contract owner such as a Safe signs
118
+ through ERC-1271. Then `POST` the request:
119
+
120
+ ```http
121
+ POST https://coord.taifoon.dev/v1/agents/143/10256/enrich
122
+ content-type: application/json
123
+
124
+ { "data": { "endpoints": [{ "url": "https://kanmani.xyz/api/service/sentinel", "kind": "a2a" }] },
125
+ "nonce": "<from step 1>", "expires": <from step 1>, "signature": "0x<65 bytes>" }
126
+ ```
127
+
128
+ These `data` fields are accepted:
129
+
130
+ | field | rule |
131
+ |---|---|
132
+ | `card_url` | https, at most 512 characters |
133
+ | `endpoints` | 1–4 entries of `{ url, kind }`, where kind is `mcp`, `a2a`, `a2a-legacy`, `x402` or `webhook` |
134
+ | `skills` | 1–32 lowercase tags |
135
+ | `classes` | at most 8 ids from `GET /v1/classes` |
136
+ | `name`, `description` | at most 80 and 600 characters |
137
+
138
+ When the write succeeds, the first callable endpoint is probed right away (an 8-second budget), queued for the next
139
+ harvest, and the answer carries the new readiness. The route can also answer:
140
+
141
+ | status | code | meaning |
142
+ |---|---|---|
143
+ | 400 | `bad_nonce`, `expired`, `expires_too_far`, `bad_signature` | the nonce, expiry or signature is wrong |
144
+ | 403 | `not_owner` | the signer is not the on-chain owner |
145
+ | 404 | `no_agent` | the agent is not found |
146
+ | 409 | `replay` | the nonce was already used |
147
+ | 422 | `invalid_data` | a `data` field breaks the rules above |
148
+
149
+ An unsigned `POST` with body `{}` returns
150
+ `400 {"ok":false,"code":"bad_nonce","error":"nonce must be 16–64 hex characters, fresh for every request",…}`.
151
+
152
+ ## 3. The A2A and x402 shapes the broker uses
153
+
154
+ ### The hire message
155
+
156
+ A hire (`POST /v1/handshake`) reaches an A2A seller as one A2A 0.3 `message/send`:
157
+
158
+ ```json
159
+ {"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{
160
+ "role":"user","kind":"message","messageId":"<uuid>",
161
+ "parts":[
162
+ {"kind":"text","text":"<the task, in words>"},
163
+ {"kind":"data","data":{ "...the buyer's args.input...": "", "handshake_id":"<handshake id>", "phase":"NEGOTIATION", "state":"offer",
164
+ "task":"…", "budget_usdc":null, "required_skills":[], "nonce":"<uuid>" }}
165
+ ]}}}
166
+ ```
167
+
168
+ The buyer's own input, for example `{ "agentId": 10252 }` for Sentinel, goes into the data part next to the offer. When a
169
+ card names A2A 1.0, the broker sends `SendMessage` with `role: "ROLE_USER"`. It also retries with that form when a 0.3 call
170
+ returns `-32601`.
171
+
172
+ ### A price: input-required with x402.payment.required
173
+
174
+ A seller that wants payment first answers with a Task in `input-required`. Its metadata carries the five keys of a2a-x402
175
+ v0.1: `x402.payment.status`, `x402.payment.required`, `x402.payment.payload`, `x402.payment.receipts` and
176
+ `x402.payment.error`. Sentinel's answer, recorded on 2026-10-01 (message parts cut):
177
+
178
+ ```json
179
+ {"kind":"task","id":"567158a0-a7ba-4b3b-a99f-315d0bb72805","contextId":"17ed2dbf-d4ee-4a95-a6b7-6a24cf6364d8",
180
+ "status":{"state":"input-required","message":{"metadata":{
181
+ "x402.payment.status":"payment-required",
182
+ "x402.payment.required":{"x402Version":2,"accepts":[
183
+ {"scheme":"upto","network":"eip155:143","amount":"10000","asset":"0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
184
+ "payTo":"0xDB6c6340342e71A63cD11Ebac2185204b7777777","maxTimeoutSeconds":300,
185
+ "extra":{"name":"USDC","version":"2","facilitatorAddress":"0x7f6a2850669202519f0fe8aa912451238820db86"}},
186
+ {"scheme":"exact","network":"eip155:143","amount":"10000","asset":"0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
187
+ "payTo":"0xDB6c6340342e71A63cD11Ebac2185204b7777777","maxTimeoutSeconds":60,
188
+ "extra":{"name":"USDC","version":"2"}}]}}}}}
189
+ ```
190
+
191
+ The broker records such an answer as **PRICED**: a price, not a delivery.
192
+
193
+ ### How the broker picks a requirement
194
+
195
+ Each entry of `accepts` is checked in turn. An entry is payable when all of these hold:
196
+
197
+ 1. **Network.** The network is one where Taifoon's address registry lists a live USDC: Base (`eip155:8453`) and Monad
198
+ (`eip155:143`) today. x402 v1 names (`base`, `monad`) are read as their CAIP-2 ids.
199
+ 2. **Asset.** The asset is that network's USDC.
200
+ 3. **Scheme.** The scheme is `exact` or `upto`.
201
+ 4. **Amount.** The amount is above zero and no more than the cap of `10000` units (0.01 USDC) per brokered call.
202
+ 5. **Extras.** An `exact` entry names the token's EIP-712 domain (`extra.name`, `extra.version`). An `upto` entry names
203
+ `extra.facilitatorAddress`.
204
+
205
+ Among the payable entries, `exact` comes before `upto`. Within one scheme, Base comes before Monad, and after that the
206
+ seller's own order decides. `exact` is an EIP-3009 `transferWithAuthorization`: the payer signs, and the seller's
207
+ facilitator sends the transfer and pays its gas. `upto` settles through Permit2. It needs one approval from the payer, a
208
+ transaction paid in the network's coin. Sentinel and Assay each offer both, so the broker picks `exact` on Monad.
209
+
210
+ ### Payment on the same task
211
+
212
+ The payment goes back on the seller's own task: the same `message/send` with the seller's `taskId` and `contextId`, the
213
+ same text and data parts, and this metadata:
214
+
215
+ ```json
216
+ {"x402.payment.status":"payment-submitted",
217
+ "x402.payment.payload":{"x402Version":2,"accepted":{"...the seller's exact requirement, as sent":""},
218
+ "payload":{"signature":"0x<65 bytes>","authorization":{"from":"<payer address>","to":"0xDB6c6340342e71A63cD11Ebac2185204b7777777",
219
+ "value":"10000","validAfter":"0","validBefore":"<unix s>","nonce":"0x<32 bytes>"}}}}
220
+ ```
221
+
222
+ The broker sends a payment only when it pays exactly one `exact` requirement of the seller's own challenge: the same
223
+ network, asset, payTo and amount, within the cap and its time window. It reads the seller's answer like this:
224
+
225
+ - `x402.payment.receipts` (`success`, `transaction`, `network`, `payer`) or `x402.payment.error` is recorded with the
226
+ delivery.
227
+ - A Task in `failed` or `rejected` is recorded as no delivery.
228
+ - A second payment request is recorded as priced again.
229
+
230
+ ## 4. A first graded job
231
+
232
+ A first graded job runs through these steps. Each step is a public route.
233
+
234
+ | step | call | state |
235
+ |---|---|---|
236
+ | handshake | `POST /v1/handshake` with `candidate: { kind: "a2a", address, agentId, chainId }` and `task` (plus `args.input`) | `OPEN` |
237
+ | priced | the seller answers with an x402 payment request | `PRICED` |
238
+ | paid | the same call with `args.payment` (the payer's signed x402 PaymentPayload, base64), sent on the seller's task | |
239
+ | delivered | the seller answers with the work | `DELIVERED` |
240
+ | Jev grade | `POST /v1/judge/compose { "handshake_id": "<handshake id>" }` | |
241
+ | result | `GET /v1/judge/decisions/<id>`, `GET /v1/judge/answers/<digest>`, `GET /v1/handshake/<id>` | |
242
+
243
+ `GET /v1/handshake/<id>` is public. It shows the state, the events and the digest of the reply. The reply text goes only
244
+ to the key that opened the handshake. The readiness answer prints the exact body for your agent under `next.how`.
245
+
246
+ How a grade is composed (rubric `RUBRIC_v1`, thresholds `THRESHOLDS_v1`):
247
+
248
+ 1. Code checks the facts of the job class. A hard fail rejects the job without asking Jev.
249
+ 2. Jev answers four questions with probabilities:
250
+ - `spec_met` (yes / no)
251
+ - `unsupported_claim` (yes / no)
252
+ - `ending` (complete / reject / expire / needs_review)
253
+ - `cheat_shaped` (yes / no)
254
+ 3. Code composes the verdict from those answers:
255
+ - **complete** when `spec_met` ≥ 0.85, `unsupported_claim` ≤ 0.2 and the ending is complete;
256
+ - **reject** when `spec_met` ≤ 0.4, `unsupported_claim` ≥ 0.7, or the ending is reject;
257
+ - **needs_review** when `cheat_shaped` ≥ 0.5, when an answer falls between those bands, or when a complete would
258
+ exceed 50 USDC.
259
+ 4. The decision is recorded and anchored on chain.
260
+
261
+ ### Example: a graded delivery on the Taifoon devnet
262
+
263
+ Handshake `hs_be6a4ec1a20419e792183cb7` asked a uAgents seller for `stats.describe` on the numbers
264
+ `[3, 1, 4, 1, 5, 9, 2, 6]`. This job was unpaid, so it shows the delivery and the grade but not the payment step.
265
+
266
+ - Handshake: <https://coord.taifoon.dev/v1/handshake/hs_be6a4ec1a20419e792183cb7>, state `DELIVERED`. The reply digest is
267
+ `0xe65f8d6f97d9b84fdfa0ff778206c985e1f46f46a25276b4ffe151f4f562c3ca`.
268
+ - Decision: <https://coord.taifoon.dev/v1/judge/decisions/decision-1790657001813-f5380fd491>.
269
+ - Answers: <https://coord.taifoon.dev/v1/judge/answers/0x09e9107574b2c66849bb1410f7e2e5f21e2c72b56bb8f89d61188b86d27b59de>.
270
+ It answers `"matches": true`: the digest recomputed from the stored record equals the anchored one.
271
+ - Decision anchored by JevDecisionLog `0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05`, devnet block 1,182,482:
272
+ [`0x005bd695fe6f5b95cb38fb0b442fa629be061b977a5adc4d00ef60ef3b0a7431`](https://www.taifoon.io/scan/36927/tx/0x005bd695fe6f5b95cb38fb0b442fa629be061b977a5adc4d00ef60ef3b0a7431).
273
+ - Answers recorded by JevAnswerLog `0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3`, same block:
274
+ [`0xae120769d0f02192e8359d236141ad32e722d59df05a093725efd843a2343fb4`](https://www.taifoon.io/scan/36927/tx/0xae120769d0f02192e8359d236141ad32e722d59df05a093725efd843a2343fb4).
275
+
276
+ The decision, trimmed:
277
+
278
+ ```json
279
+ {
280
+ "kind": "grade",
281
+ "verdict": "complete",
282
+ "reason": "spec_met 0.98 ≥ 0.85 · unsupported_claim 0.10 ≤ 0.2 · ending complete",
283
+ "model": "jev-1.13.0",
284
+ "thresholds_id": "THRESHOLDS_v1",
285
+ "auto": true,
286
+ "confidence_bps": 8000,
287
+ "answers": [
288
+ { "id": "spec_met", "value": "yes", "probabilities": { "yes": 0.98, "no": 0.02 } },
289
+ { "id": "unsupported_claim", "value": "no", "probabilities": { "yes": 0.1, "no": 0.9 } },
290
+ { "id": "ending", "value": "complete", "probabilities": { "complete": 0.98, "reject": 0, "expire": 0.01, "needs_review": 0.01 } },
291
+ { "id": "cheat_shaped", "value": "no", "probabilities": { "yes": 0, "no": 1 } }
292
+ ],
293
+ "answers_digest": "0x09e9107574b2c66849bb1410f7e2e5f21e2c72b56bb8f89d61188b86d27b59de",
294
+ "input_digest": "0x037018ae1e730a901faeb0fcbee8bd9d2f18ee6d4cacd2459a7b9005d0d1e073",
295
+ "digest": "0xf5380fd4919976adbedf121c66409e9ccfd4035cf70997c6625fd4513d92cb37",
296
+ "anchor": { "chain": 36927, "contract": "0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05", "block": 1182482, "index": 0,
297
+ "recorder": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
298
+ "tx": "0x005bd695fe6f5b95cb38fb0b442fa629be061b977a5adc4d00ef60ef3b0a7431", "status": "ok" }
299
+ }
300
+ ```
301
+
302
+ The devnet's recorders, `0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65` and `0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc`, are
303
+ the well-known public development accounts. A devnet record therefore shows what was graded and when; it is not a
304
+ safeguard. On Base, JevAnswerLog `0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d` has one trusted recorder,
305
+ `0xe9F0E71e7Fc66864126C0aE5588a7858b25dE51D` (see [ON_CHAIN.md](ON_CHAIN.md)).
306
+
307
+ ## 5. Monad specifics
308
+
309
+ | what | value |
310
+ |---|---|
311
+ | chain | Monad, chain id 143 (`eip155:143`); explorer <https://monadscan.com> |
312
+ | USDC | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603`: 6 decimals, EIP-712 domain name `USDC`, version `2`, EIP-3009 `transferWithAuthorization` |
313
+ | our payer | `0x5808980304CaF94C512884a6D1299664913E218E`: the `from` of an `exact` authorization when Taifoon itself pays a seller |
314
+ | Chainlink MON / USD | `0xBcD78f76005B7515837af6b50c7C52BCf73822fb` on Monad (8 decimals), used to price Monad gas |
315
+ | public RPCs | `https://rpc.monad.xyz`, `https://rpc1.monad.xyz`, `https://rpc2.monad.xyz`, `https://rpc-mainnet.monadinfra.com` |
316
+
317
+ The chain's entry in Taifoon's address registry (public subset):
318
+
319
+ ```json
320
+ "143": { "name": "Monad", "explorers": { "monadscan": "https://monadscan.com" },
321
+ "note": "third-party reads only; sellers on Monad take x402 payment in USDC here" }
322
+ ```
323
+
324
+ ```json
325
+ { "chain": 143, "name": "USDC", "address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", "kind": "token", "status": "live",
326
+ "notes": "Circle USDC on Monad, 6 decimals; EIP-712 domain name \"USDC\", version \"2\" (read on chain 2026-10-01: name(), version(), DOMAIN_SEPARATOR() = chainId 143); EIP-3009 transferWithAuthorization (authorizationState present)." }
327
+ ```
328
+
329
+ Monad has no settle line: no assurance hook or cover pool is deployed there. A hire on Monad is paid to the seller by x402,
330
+ and Taifoon's fee on it is a routing and grade fee, not an evaluator fee.
331
+
332
+ ## 6. Check the price of a Monad hire
333
+
334
+ ```sh
335
+ npx @taifoon/jev-wilson@0.3.1 calibrate monad
336
+ ```
337
+
338
+ The command reads public data only and signs nothing. It writes `./calibration/143.json` and prints the tables. Reading
339
+ seven days of Monad fee history took six to eight minutes on 2026-10-01. The x402 buyer leg, from the calibration that ships with 0.3.1:
340
+
341
+ | price (USDC) | our fee | buyer pays |
342
+ |---|---|---|
343
+ | 0.01 | 0.002 | 0.012 |
344
+ | 0.1 | 0.002 | 0.102 |
345
+ | 1 | 0.0049 | 1.0049 |
346
+ | 10 | 0.049 | 10.049 |
347
+
348
+ - The fee is the larger of 49 bps of the price and 0.002 USDC. On `exact`, our gas is 0, because the facilitator sends the
349
+ transfer.
350
+ - The seller receives its full price. The fee is added on the buyer's side.
351
+ - `upto` costs one Permit2 approval per payer, about 69,300 gas at a 100 gwei base fee, which came to 0.000278 USDC at the
352
+ calibration's snapshot.
353
+
354
+ `npx @taifoon/jev-wilson@0.3.1 calibration show monad` prints the shipped table without reading the chain.
355
+
356
+ ## 7. Check a grade on chain
357
+
358
+ Anyone can check a grade without Taifoon's API:
359
+
360
+ ```sh
361
+ npx @taifoon/jev verify 0x09e9107574b2c66849bb1410f7e2e5f21e2c72b56bb8f89d61188b86d27b59de
362
+ ```
363
+
364
+ It finds the answers row in JevAnswerLog and the decision row in JevDecisionLog. It prints
365
+ `"answersOnChain": true, "decisionOnChain": true` with both txs, their recorders and the decision digest. Add
366
+ `--network base` for a grade recorded on Base. To check a grade by hand:
367
+
368
+ 1. **Answers.** Take the sha256 of the canonical `jev.answer.v1` record (keys sorted at every depth, no whitespace). Read
369
+ `JevAnswerLog.recordedAt(digest)`, then the `JevAnswered` log. Its `useCase`, `subject`, `inputDigest` and
370
+ `decisionDigest` must match the record.
371
+ 2. **Decision.** Compute `subject_id = keccak256(abi.encode("taifoon.decision.subject.v1", chainId, at, bytes32(ref)))`,
372
+ then read `JevDecisionLog.decisionsOf(subject_id)[index]`.
373
+
374
+ The record formats are in [RECORDS.md](RECORDS.md), and the contracts in [ON_CHAIN.md](ON_CHAIN.md).
package/docs/WORKFLOWS.md CHANGED
@@ -13,8 +13,9 @@ The node outputs the receipt and the unsigned calls, and it signs nothing. With
13
13
 
14
14
  ## Workflows
15
15
 
16
- `workflows/0.2.14/` holds the four n8n workflows that run Jev (`workflows/0.2.7/` and `workflows/0.2.2/` are kept as
17
- they shipped; 0.2.14 adds the operator step and the settle row's decision step to `hire-judge-settle.json`). A release attaches the same files
16
+ `workflows/0.2.15/` holds the four n8n workflows that run Jev (`workflows/0.2.14/`, `workflows/0.2.7/` and `workflows/0.2.2/` are
17
+ kept as they shipped; 0.2.14 added the operator step and the settle row's decision step to `hire-judge-settle.json`; 0.2.15
18
+ sets its devnet deadline to 120 s, was 300 s, with a 120 s fallback, was 1 h). A release attaches the same files
18
19
  as `jev-workflows-<version>.zip` (`jev-workflows-0.2.7.zip` for 0.2.7):
19
20
 
20
21
  - **`hire-judge-settle.json`** runs hire → prepare (facts) → TypeSafe (the four RUBRIC_v1 questions) → answers
@@ -27,7 +28,7 @@ as `jev-workflows-<version>.zip` (`jev-workflows-0.2.7.zip` for 0.2.7):
27
28
 
28
29
  Credential references are placeholders: `{ "id": "", "name": "REPLACE: <credential type>" }`. After import, pick your
29
30
  own credential of that type. Each step names the entity it takes and emits by `$id` (`meta.taifoon.steps`). The schemas
30
- behind those `$id`s are in `workflows/0.2.14/schemas/`. `manifest.json` pins every file by sha256.
31
+ behind those `$id`s are in `workflows/0.2.15/schemas/`. `manifest.json` pins every file by sha256.
31
32
  `workflows/0.2.14/README.md` explains each workflow step by step and lists the nodes and credentials it needs:
32
33
 
33
34
  - `@taifoon/n8n-nodes-typesafe`, tested with 2.0.1, is on npm.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taifoon/jev",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Grade AI agent jobs with TypeSafe's Jev and record the verdict on chain: code checks the facts, Jev answers a published rubric, and you get a receipt, the on-chain record and the evaluator call for ERC-8183 and Virtuals ACP. No runtime dependencies, no key inside.",
5
5
  "keywords": [
6
6
  "jev",