@taifoon/jev 0.2.13 → 0.2.15

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.
Files changed (34) hide show
  1. package/README.md +4 -3
  2. package/dist/verify.d.ts +14 -0
  3. package/dist/verify.js +46 -5
  4. package/docs/SELLERS.md +374 -0
  5. package/docs/WORKFLOWS.md +6 -4
  6. package/package.json +3 -2
  7. package/workflows/0.2.14/README.md +109 -0
  8. package/workflows/0.2.14/batch-judge.json +341 -0
  9. package/workflows/0.2.14/hire-judge-settle.json +920 -0
  10. package/workflows/0.2.14/jev-grader.json +433 -0
  11. package/workflows/0.2.14/manifest.json +152 -0
  12. package/workflows/0.2.14/schemas/answers.json +230 -0
  13. package/workflows/0.2.14/schemas/common.json +397 -0
  14. package/workflows/0.2.14/schemas/delivery.json +217 -0
  15. package/workflows/0.2.14/schemas/facts.json +209 -0
  16. package/workflows/0.2.14/schemas/hire.json +314 -0
  17. package/workflows/0.2.14/schemas/job.json +88 -0
  18. package/workflows/0.2.14/schemas/settlement.json +243 -0
  19. package/workflows/0.2.14/schemas/verdict.json +296 -0
  20. package/workflows/0.2.14/stamp-grade.json +175 -0
  21. package/workflows/0.2.15/README.md +109 -0
  22. package/workflows/0.2.15/batch-judge.json +341 -0
  23. package/workflows/0.2.15/hire-judge-settle.json +920 -0
  24. package/workflows/0.2.15/jev-grader.json +433 -0
  25. package/workflows/0.2.15/manifest.json +152 -0
  26. package/workflows/0.2.15/schemas/answers.json +230 -0
  27. package/workflows/0.2.15/schemas/common.json +397 -0
  28. package/workflows/0.2.15/schemas/delivery.json +217 -0
  29. package/workflows/0.2.15/schemas/facts.json +209 -0
  30. package/workflows/0.2.15/schemas/hire.json +314 -0
  31. package/workflows/0.2.15/schemas/job.json +88 -0
  32. package/workflows/0.2.15/schemas/settlement.json +243 -0
  33. package/workflows/0.2.15/schemas/verdict.json +296 -0
  34. package/workflows/0.2.15/stamp-grade.json +175 -0
package/README.md CHANGED
@@ -14,8 +14,8 @@ npx @taifoon/jev run --demo
14
14
 
15
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)
16
16
 
17
- Then grade your own with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). No runtime
18
- dependencies, no key inside, and it signs nothing. Recording a grade on chain is optional.
17
+ Then grade your own with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). Installing it also
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.
19
19
 
20
20
  ## Quick start
21
21
 
@@ -159,6 +159,7 @@ 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
165
  - [On chain: contract addresses on Base and devnet, and one real grade end to end](docs/ON_CHAIN.md)
@@ -176,4 +177,4 @@ npx @taifoon/jev workflows export ./jev-workflows
176
177
 
177
178
  Independent project. Jev and TypeSafe are products of TypeSafe AI, Inc., which does not endorse this package.
178
179
 
179
- Built by Taifoon. MIT. The package's only runtime dependency is Taifoon's own @taifoon/jev-wilson, the pricing; nothing third-party. It contains no key. It calls Jev only with your own TypeSafe key.
180
+ Built by Taifoon. MIT. Its runtime dependencies are Taifoon's own packages, @taifoon/jev-wilson (the pricing) and @taifoon/n8n-nodes-typesafe; nothing third-party. It contains no key. It calls Jev only with your own TypeSafe key.
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
  }
@@ -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,10 @@ 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.7/` holds the four n8n workflows that run Jev (`workflows/0.2.2/` is kept as it shipped). The same
17
- files are attached to the GitHub release as `jev-workflows-0.2.7.zip`:
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
19
+ as `jev-workflows-<version>.zip` (`jev-workflows-0.2.7.zip` for 0.2.7):
18
20
 
19
21
  - **`hire-judge-settle.json`** runs hire → prepare (facts) → TypeSafe (the four RUBRIC_v1 questions) → answers
20
22
  (verdict, recorded; Record On = devnet anchors it) → settle on devnet. Executions 86 and 88 of this workflow are the
@@ -26,8 +28,8 @@ files are attached to the GitHub release as `jev-workflows-0.2.7.zip`:
26
28
 
27
29
  Credential references are placeholders: `{ "id": "", "name": "REPLACE: <credential type>" }`. After import, pick your
28
30
  own credential of that type. Each step names the entity it takes and emits by `$id` (`meta.taifoon.steps`). The schemas
29
- behind those `$id`s are in `workflows/0.2.7/schemas/`. `manifest.json` pins every file by sha256.
30
- `workflows/0.2.7/README.md` explains each workflow step by step and lists the nodes and credentials it needs:
31
+ behind those `$id`s are in `workflows/0.2.15/schemas/`. `manifest.json` pins every file by sha256.
32
+ `workflows/0.2.14/README.md` explains each workflow step by step and lists the nodes and credentials it needs:
31
33
 
32
34
  - `@taifoon/n8n-nodes-typesafe`, tested with 2.0.1, is on npm.
33
35
  - `n8n-nodes-taifoon`, tested with 0.4.5, and `n8n-nodes-taifoon-devnet-signer`, tested with 0.3.0, are custom
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taifoon/jev",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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",
@@ -70,7 +70,8 @@
70
70
  "access": "public"
71
71
  },
72
72
  "dependencies": {
73
- "@taifoon/jev-wilson": "^0.2.0"
73
+ "@taifoon/jev-wilson": "^0.2.0",
74
+ "@taifoon/n8n-nodes-typesafe": "^2.0.4"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/node": "^25.9.2",