@taifoon/jev 0.2.14 → 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.
package/README.md CHANGED
@@ -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)
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,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.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",
@@ -0,0 +1,109 @@
1
+ # Jev workflows 0.2.15
2
+
3
+ The n8n workflows that run Jev on n8n.taifoon.dev, exported with credential references replaced by placeholders (`{ "id": "", "name": "REPLACE: <credential type>" }`: pick your own credential of that type after import). Import with n8n → Workflows → Import from File, or `n8n import:workflow --input=<file>`. Every step carries its contract: `meta.taifoon.steps` names the canonical entity it takes and emits by `$id`; the schemas are in `schemas/` (the `$id`s are identifiers, the files are here).
4
+
5
+ ## Nodes they need
6
+
7
+ | Package | Tested with | Where |
8
+ |---|---|---|
9
+ | `@taifoon/n8n-nodes-typesafe` | 2.0.1 | npm (community node): npm i @taifoon/n8n-nodes-typesafe |
10
+ | `n8n-nodes-taifoon` | 0.4.5 | custom extension (type prefix CUSTOM.); not on npm |
11
+ | `n8n-nodes-taifoon-devnet-signer` | 0.3.0 | custom extension (type prefix CUSTOM.); not on npm |
12
+
13
+ ## Credentials they need
14
+
15
+ | Type | What |
16
+ |---|---|
17
+ | `taifoonTypeSafeApi` | your TypeSafe key (console.typesafe.ai) — credential type "TypeSafe API" of @taifoon/n8n-nodes-typesafe |
18
+ | `taifoonRelayerApi` | a Taifoon relayer key (tfr_…) for https://www.taifoon.io/v1 — credential type "Taifoon Relayer API" of n8n-nodes-taifoon |
19
+ | `taifoonDevnetKeyApi` | a devnet 36927 key with gas from https://faucet.taifoon.dev — credential type "Taifoon Devnet Key API" of n8n-nodes-taifoon-devnet-signer |
20
+
21
+ ## HIRE → JUDGE → SETTLE (devnet 36927) — `hire-judge-settle.json`
22
+
23
+ One hire from offer to settlement. The seller’s reply is held with its digest; prepare decides the facts (a hard fail skips Jev); your TypeSafe credential answers the four RUBRIC_v1 questions; answers composes the verdict under THRESHOLDS_v1, records decision.v2 and jev.answer.v1 and, with Record On = devnet (set in this file), anchors them on the devnet logs; a complete settles through the devnet assurance hook (approve, fund, submit, complete), needs_review is held until the job’s deadline, when anyone may call expire. Executions 86 and 88 of this workflow are the golden runs @taifoon/jev re-derives.
24
+
25
+ **Needs:** n8n-nodes-taifoon ≥ 0.4.5 and n8n-nodes-taifoon-devnet-signer ≥ 0.3.0 (custom extensions), @taifoon/n8n-nodes-typesafe ≥ 2.0.1; a Taifoon relayer key, your TypeSafe key, and two devnet 36927 keys (buyer, seller) with gas and dUSDC. **Outputs:** the whole JOB (Receipt, then Proofs · wait): handshake, verdict, decision with its devnet anchor tx, and every settlement transaction with its proof link.
26
+
27
+ - source: `tfnhiresettle001` on n8n.taifoon.dev · sha256 `b13b48369fb6cdba88371c22d5aba8710eab414ac30a818c2a8189116b86c94b`
28
+ - nodes: `n8n-nodes-taifoon`, `@taifoon/n8n-nodes-typesafe`, `n8n-nodes-taifoon-devnet-signer`; built-ins manualTrigger v1, set v3.4, code v2, if v2.2, httpRequest v4.2
29
+ - credentials: `taifoonRelayerApi`, `taifoonTypeSafeApi`, `taifoonDevnetKeyApi`
30
+
31
+ | Step | Takes | Emits | Does |
32
+ |---|---|---|---|
33
+ | Start | — | — | start a run (swap for a Webhook or Schedule trigger) |
34
+ | Inputs | — | hire.json | the hire intent: task, candidate seller, endpoint/tool, buyer, price, chain (a HIRE in state Open) |
35
+ | Offer + deliver | hire.json | job.json | POST /v1/handshake {dispatch:true}: the offer goes to the seller’s own endpoint; the reply is held with its keccak digest |
36
+ | Delivery | job.json | delivery.json | stop unless a reply digest is held (no digest = nothing to settle) |
37
+ | Judge · prepare | delivery.json | facts.json | POST /v1/judge/compose (prepare): the facts decide first; hard_fail ends here; else the exact text Jev reads + prepare_digest |
38
+ | Hard fail? | facts.json | facts.json | branch: a hard fail goes straight to the verdict without asking Jev |
39
+ | Jev · TypeSafe (our credential) | facts.json | answers.json | TypeSafe ask on the prepared text: every RUBRIC_ASKED answer with its whole distribution |
40
+ | Judge · answers | answers.json | verdict.json | POST /v1/judge/compose (answers): recompose under THRESHOLDS_v1, record decision.v2, anchor on JevDecisionLog |
41
+ | Verdict | verdict.json + facts.json | verdict.json | the composed verdict (complete \| reject \| needs_review) with what settlement needs carried forward |
42
+ | Settle | verdict.json | settlement.json | POST /v1/settle: the plan (jobId, price, premium, deposit, calls[] in order, who signs each) |
43
+ | Seller · approve deposit | settlement.json | settlement.json | the seller approves D (> P); the tx joins settlement.txs |
44
+ | Buyer · approve + fund | settlement.json | settlement.json | the buyer approves P + π and calls fundJob: machine Delivered → Funded |
45
+ | Seller · submit | settlement.json | settlement.json | submit(jobId, evidenceDigest = delivery.digest): machine Funded → Submitted |
46
+ | Verdict says complete? | settlement.json | settlement.json | branch on the composed verdict: complete → complete; otherwise hold (Submitted → Held) |
47
+ | Buyer · complete + capture | settlement.json | settlement.json | complete(jobId) (+ capture): machine → Settled |
48
+ | Settle row · decided | settlement.json | settlement.json | the settle row names the decision, not a document: POST /v1/settle/decided { jobId, decision_id, end_tx = the complete transaction } on the lane’s relayer key. The layer writes decided_by "jev" only when the chain proves it (an automatic complete, the decision’s anchor on the JevDecisionLog, the job bound to the handshake and its sealed reply, Completed for this job); it never errors the run |
49
+ | Held for review | settlement.json | settlement.json | needs_review / reject: funded and sealed, nothing paid; the operator step (a record, then the evaluator’s postVerdict) or the deadline decides |
50
+ | Receipt | settlement.json | job.json | the whole JOB: hire, delivery, facts, verdict, settlement — every transaction with its proof link |
51
+ | Proofs · wait | job.json | job.json | GET /v1/proof/tx/36927/<tx> for every transaction of the run; pending is retried with backoff (5 s -> 60 s, <= 240 s), never reported as a gap |
52
+ | Operator step · open | settlement.json | settlement.json | the evaluator leg, not a document: POST /v1/settle/evaluate {open:true} opens the operator step of a job held on needs_review. The workflow does not decide: the step waits for an operator record (decided_by loop-agent on the devnet, or operator with the operator key), anchored on chain before the evaluator posts. A composed reject is not an operator step (the layer answers not_needs_review) |
53
+
54
+ ## Jev on-chain grader (grade → stamp → devnet) — `jev-grader.json`
55
+
56
+ Grades Base jobs that were paid and delivered and that nobody ruled on (GET /v1/judge/ready): four per run, each through prepare → Jev → answers, then the canonical verdict, then GradeStampRegistry.stamp on the devnet, signed by the devnet signer node; the stamp transaction is appended to the verdict’s anchors.
57
+
58
+ - source: `tfnjevgrader0001` on n8n.taifoon.dev · sha256 `762248c554dd0607362e20361ec3efe0fda58326d89c2f1c18c525ea48e92e35`
59
+ - nodes: `n8n-nodes-taifoon`, `@taifoon/n8n-nodes-typesafe`, `n8n-nodes-taifoon-devnet-signer`; built-ins manualTrigger v1, code v2, if v2.2
60
+ - credentials: `taifoonRelayerApi`, `taifoonTypeSafeApi`, `taifoonDevnetKeyApi`
61
+
62
+ | Step | Takes | Emits | Does |
63
+ |---|---|---|---|
64
+ | When clicking ‘Execute workflow’ | — | — | start a grading run |
65
+ | Ready jobs (Base) | — | job.json | GET /v1/judge/ready: jobs on Base that were paid and delivered, and nobody ruled |
66
+ | Pick 4 calibrated | job.json | job.json | keep four whose seller has a calibrated record; one item per job, no state built here |
67
+ | Judge · prepare | job.json | facts.json | POST /v1/judge/compose (prepare) on the job: facts decide first; the pack comes from the job evidence (GRADEABLE labels 1–21) |
68
+ | Hard fail? | facts.json | facts.json | a hard fail is final at prepare (Jev is not asked; nothing to stamp) |
69
+ | Jev · TypeSafe (our credential) | facts.json | answers.json | TypeSafe ask on the prepared text (jevStateOf): every RUBRIC_ASKED answer with its whole distribution |
70
+ | Judge · answers | answers.json | verdict.json | POST /v1/judge/compose (answers): recompose under THRESHOLDS_v1, record decision.v2 and the answers, anchor both |
71
+ | Verdict | verdict.json | verdict.json | the recorded decision as a canonical VERDICT (fromDecision), composed from this run’s receipt |
72
+ | Stamp Grade (unsigned) | verdict.json | verdict.json | POST /v1/assurance/call stampGrade: the unsigned GradeStampRegistry.stamp(subject_id, digest, verdict code, mode 2) |
73
+ | Taifoon Devnet Signer | verdict.json | verdict.json | sign and send the stamp on devnet 36927 (anvil #0) |
74
+ | Verdict + stamp | verdict.json | verdict.json | the same VERDICT with the stamp transaction appended to anchors |
75
+
76
+ ## Batch judge — `batch-judge.json`
77
+
78
+ A batch of subjects, each as its own prepare → Jev → answers (so every answer is recorded), aggregated and posted to POST /v1/judge/batch.
79
+
80
+ - source: `tfnbatchjudge001` on n8n.taifoon.dev · sha256 `8e66a8e261e9f1f8052bd0b7658cb2ef5212d8df89b8a5347b56762701f078fd`
81
+ - nodes: `n8n-nodes-taifoon`, `@taifoon/n8n-nodes-typesafe`; built-ins manualTrigger v1, code v2, if v2.2, httpRequest v4.2
82
+ - credentials: `taifoonRelayerApi`, `taifoonTypeSafeApi`
83
+
84
+ | Step | Takes | Emits | Does |
85
+ |---|---|---|---|
86
+ | Grade a batch | — | — | start a batch (pass { subjects, jobId, threshold } or run the demo set) |
87
+ | Build the batch | — | hire.json | one canonical HIRE per subject (a delivered handshake, or a job on chain); no state is built here |
88
+ | Judge · prepare | hire.json | facts.json | POST /v1/judge/compose (prepare) per subject: the facts decide first; the pack is built from DELIVERY + FACTS; else the exact text Jev reads + prepare_digest |
89
+ | Hard fail? | facts.json | facts.json | branch: a hard fail is final (Jev is not asked) and goes straight to the batch |
90
+ | Jev · TypeSafe (our credential) | facts.json | answers.json | TypeSafe ask on the prepared text (jevStateOf): every RUBRIC_ASKED answer with its whole distribution |
91
+ | Judge · answers | answers.json | verdict.json | POST /v1/judge/compose (answers): recompose under THRESHOLDS_v1, record decision.v2 and the answers (jev.answer.v1), anchor both |
92
+ | Grade + aggregate | facts.json + verdict.json | — | the batch outcome from the recorded verdicts (Conformant / NonConformant / NoGrade counts, fail fraction vs threshold) — a batch record, not a canonical entity |
93
+ | Record the batch to the coordination layer | — | — | POST /v1/judge/batch (relayer key): the batch record, keyed batch:<jobId> |
94
+
95
+ ## Stamp Grade (a recorded verdict → GradeStampRegistry) — `stamp-grade.json`
96
+
97
+ Takes one recorded decision (by id), maps it to the canonical verdict, and stamps it on the devnet GradeStampRegistry; emits the verdict with the stamp transaction in its anchors.
98
+
99
+ - source: `tfnstamp00000001` on n8n.taifoon.dev · sha256 `516d9483e3d26e1891dacb8c4493d609340e3c6a3b200784c72864f9cb931ebc`
100
+ - nodes: `n8n-nodes-taifoon`, `n8n-nodes-taifoon-devnet-signer`; built-ins manualTrigger v1, code v2
101
+ - credentials: `taifoonDevnetKeyApi`
102
+
103
+ | Step | Takes | Emits | Does |
104
+ |---|---|---|---|
105
+ | Start | — | — | start (pass { decision_id } to stamp another verdict) |
106
+ | Verdict | — | verdict.json | GET /v1/judge/decisions/:id → the canonical VERDICT (fromDecision) and its stamp code |
107
+ | Stamp Grade | verdict.json | verdict.json | POST /v1/assurance/call stampGrade: the unsigned GradeStampRegistry.stamp(subject_id, digest, verdict code, mode) |
108
+ | Taifoon Devnet Signer | verdict.json | verdict.json | sign and send the stamp on devnet 36927 (anvil #0) |
109
+ | Verdict + stamp | verdict.json | verdict.json | the same VERDICT with the stamp transaction appended to anchors |