@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.
- package/README.md +4 -3
- package/dist/verify.d.ts +14 -0
- package/dist/verify.js +46 -5
- package/docs/SELLERS.md +374 -0
- package/docs/WORKFLOWS.md +6 -4
- package/package.json +3 -2
- package/workflows/0.2.14/README.md +109 -0
- package/workflows/0.2.14/batch-judge.json +341 -0
- package/workflows/0.2.14/hire-judge-settle.json +920 -0
- package/workflows/0.2.14/jev-grader.json +433 -0
- package/workflows/0.2.14/manifest.json +152 -0
- package/workflows/0.2.14/schemas/answers.json +230 -0
- package/workflows/0.2.14/schemas/common.json +397 -0
- package/workflows/0.2.14/schemas/delivery.json +217 -0
- package/workflows/0.2.14/schemas/facts.json +209 -0
- package/workflows/0.2.14/schemas/hire.json +314 -0
- package/workflows/0.2.14/schemas/job.json +88 -0
- package/workflows/0.2.14/schemas/settlement.json +243 -0
- package/workflows/0.2.14/schemas/verdict.json +296 -0
- package/workflows/0.2.14/stamp-grade.json +175 -0
- package/workflows/0.2.15/README.md +109 -0
- package/workflows/0.2.15/batch-judge.json +341 -0
- package/workflows/0.2.15/hire-judge-settle.json +920 -0
- package/workflows/0.2.15/jev-grader.json +433 -0
- package/workflows/0.2.15/manifest.json +152 -0
- package/workflows/0.2.15/schemas/answers.json +230 -0
- package/workflows/0.2.15/schemas/common.json +397 -0
- package/workflows/0.2.15/schemas/delivery.json +217 -0
- package/workflows/0.2.15/schemas/facts.json +209 -0
- package/workflows/0.2.15/schemas/hire.json +314 -0
- package/workflows/0.2.15/schemas/job.json +88 -0
- package/workflows/0.2.15/schemas/settlement.json +243 -0
- package/workflows/0.2.15/schemas/verdict.json +296 -0
- 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
|

|
|
16
16
|
|
|
17
|
-
Then grade your own with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai).
|
|
18
|
-
|
|
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.
|
|
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
|
-
/**
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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/SELLERS.md
ADDED
|
@@ -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.
|
|
17
|
-
|
|
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.
|
|
30
|
-
`workflows/0.2.
|
|
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.
|
|
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",
|