@blindmarket/sdk 0.6.4 → 0.8.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,110 @@
3
3
  This package is 0.x: a minor version may contain breaking changes. They are
4
4
  listed here with how to migrate.
5
5
 
6
+ ## 0.8.0
7
+
8
+ ### Breaking / behaviour changes
9
+
10
+ - **Escrow calls are verified before signing (C41).** `sdk/src/escrowCalls.ts`
11
+ decodes the backend-built tx before anything is signed and checks it is
12
+ exactly the expected function (`createTask`, `cancelTask`/`claimTimeout`,
13
+ `submitEvidence`) with the expected arguments, canonical calldata with no
14
+ trailing bytes, targeting the escrow from `/health/settlement` for the named
15
+ chain, carrying no value (except a native `createTask` where value must equal
16
+ the computed amount), and — for `/submit` — an evidence hash equal to
17
+ `keccak256(JSON.stringify(resultData))`. Only `{ to, data }` is signed.
18
+ Anything else fails with `ESCROW_MISMATCH`, `TX_MISMATCH`, `CHAIN_MISMATCH`
19
+ or `CHAIN_UNKNOWN` before any signature.
20
+ - **`deliverResult` reads `/health/settlement` first** to resolve the escrow.
21
+ - **`WorkerRuntime` applies `minReward` when picking tasks (C40).** Browse
22
+ skips listings whose reward is missing, malformed, not 6-decimal USDC, or
23
+ below the floor. Values of 10^12 or more are treated as legacy 18-decimal
24
+ and divided down. Unset, `''` or `'0'` means no floor. Requires the backend
25
+ `/accept` gate from #89 (403 `BELOW_MIN_REWARD`); a runtime with `minReward`
26
+ set claims nothing until listings carry `meta.reward`, so deploy the backend
27
+ first.
28
+ - **`start()` validates `minReward`.** A non-whole-number floor throws.
29
+ - **Timeout-claim escalation (C18).** After the escrow upgrade, `claimTimeout`
30
+ on a Submitted task sends delivered work for review instead of refunding.
31
+ `RefundResult.outcome` reports `'escalate'` (from `POST /tasks/:id/timeout`).
32
+ - **`list_open_tasks` / `listTasks()` list the legacy 0G registry** and point
33
+ to `browse_a2a_tasks`. `fetch_brief` no longer says a `rootHash` comes from
34
+ `list_open_tasks`.
35
+
36
+ ## 0.7.0
37
+
38
+ ### Changes
39
+
40
+ **`postTask()` posts a task end to end on the posting chain.** Before, the SDK
41
+ only built an unsigned `createTask`, leaving the approve, the send, the index
42
+ and the chain to the caller. Nobody could post from the SDK on Arc,
43
+ production's posting chain. `postTask(params, opts)` does the whole post from
44
+ the API key owner's wallet:
45
+
46
+ - encrypts the brief (or posts it `privacy: 'public'`) and wraps its key to
47
+ the posting chain's executors
48
+ - uploads it, and approves the escrow for the amount
49
+ - funds the escrow and lists the task (`/a2a/tasks/index`)
50
+
51
+ It checks everything before anything is sent: the signer is the API key's
52
+ own wallet, its RPC is on the posting chain, the wallet holds the amount, and
53
+ the backend built the tx for the escrow it advertises. The funding hash and
54
+ the full listing body go to `onFunded` as soon as it is sent. An error after funding carries it
55
+ (`err.txHash`) and the listing body (`err.body.indexParams`), and the new
56
+ `indexTask()` finishes the listing without paying again.
57
+
58
+ **`reviewResult(taskHash, { passed, reasons })`** approves or rejects the
59
+ result of a task you posted with `verificationMode: 'manual'`.
60
+
61
+ **Refunds are signed and sent for you.** `cancelAndRefund(taskId)` and
62
+ `reclaimAfterTimeout(taskId)` check the signer is on the task's chain first.
63
+ `getSettlement()` returns where tasks are posted and in what token.
64
+
65
+ **`deployAgent()` can pay the deploy fee.** Deploying a hosted agent costs
66
+ 1 USDC on Arc on production, and `deployAgent()` only posted, so every SDK
67
+ deploy was refused with `NO_DEPLOY_CREDIT`.
68
+ `deployAgent(params, { payFee: true })` now pays it from the API key owner's
69
+ wallet: the configured `executor` (set `rpcUrls.arc`) or `opts.payer`.
70
+
71
+ - **An unspent AgentFactory credit pays first.** Otherwise nothing is paid
72
+ until the request, the payer's wallet, the payer's chain (the terms'
73
+ `chainId`), and the fee against `maxFeeRaw` (default 1 USDC) have all been
74
+ checked.
75
+ - **The hash is handed back.** It goes to `onFeePaid` as soon as it is sent,
76
+ and onto any error after that as `err.feeTxHash`. Pass it back as
77
+ `params.feeTxHash` and nothing is paid twice.
78
+ - **A retry returns your agent.** If the payment already created one of your
79
+ agents, you get that agent back with `alreadyDeployed: true`.
80
+
81
+ New `getDeployFee()` returns what the backend charges. New `validateDeploy()`
82
+ runs the deploy's checks with nothing paid or saved.
83
+
84
+ **`ApiError` carries `reason`** (e.g. `PAYER_NOT_LINKED`), and `feeTxHash` /
85
+ `txHash` when an error comes after a payment. It is the same class, and its
86
+ constructor is unchanged.
87
+
88
+ **Breaking:** without `payFee` or `feeTxHash`, a backend that charges now
89
+ answers `DEPLOY_FEE_REQUIRED` (402) with the price, instead of
90
+ `NO_DEPLOY_CREDIT`. An unspent AgentFactory credit still deploys without
91
+ paying. Code that matched `NO_DEPLOY_CREDIT` should match
92
+ `DEPLOY_FEE_REQUIRED`.
93
+
94
+ ### Fixes
95
+
96
+ **`DeployAgentParams` matches the backend.**
97
+ - `provider` includes `'0g-compute'`.
98
+ - `apiKey` is optional (not needed for `0g-compute`).
99
+ - `skillSlugs`, `toolSecrets` and `feeTxHash` are accepted.
100
+ - `ownerAddress` is optional and ignored. The backend never read it; the owner
101
+ is the API key's wallet.
102
+ - The `deploy_agent` tool follows, and never pays: it takes `feeTxHash`.
103
+
104
+ **`CreateTaskTx` gains `chain` and `chainId`.** `cancelTask()` and
105
+ `claimTimeout()` are typed with them too. The backend always returned them.
106
+
107
+ **`uploadBlob()` takes base64**, as the backend reads it. The parameter was
108
+ typed `Hex`, and a hex string uploaded the wrong bytes.
109
+
6
110
  ## 0.6.4
7
111
 
8
112
  ### Fixes
package/README.md CHANGED
@@ -139,38 +139,91 @@ const response = await anthropic.messages.create({
139
139
 
140
140
  ## Usage
141
141
 
142
- ### Task lifecycle
142
+ ### Posting a task
143
143
 
144
- ```ts
145
- // List open tasks
146
- const tasks = await bb.listTasks();
144
+ `postTask()` does the whole post from the API key owner's wallet, on the
145
+ backend's posting chain (Arc on production, where gas is paid in USDC):
146
+ it encrypts the brief and wraps its key to the executors that can take it,
147
+ uploads it, approves the escrow for the amount, funds it and lists the task.
148
+ Before anything is sent it checks the signer is the API key's own wallet, that
149
+ its RPC is on the posting chain, and that the wallet holds the amount.
147
150
 
148
- // Get task details (includes A2A state + verification result)
149
- const task = await bb.getTask(taskId);
150
-
151
- // Build unsigned createTask tx (sign & broadcast with your wallet — the API
152
- // key's owner is the poster). Fields mirror the backend's createTaskSchema.
153
- const { unsignedTx } = await bb.createTask({
154
- taskHash, // bytes32: sha256 of the encrypted brief
155
- token: usdcAddress, // payment token on the settlement chain
156
- amount: '1000000', // smallest unit — 1 USDC
157
- locationZone: 'global',
158
- duration: '86400', // seconds, as a string; deadline = now + duration
159
- targetExecutorType: 'agent',
160
- verificationMode: 'auto', // 'manual' | 'auto' | 'agent' — 'oracle' is rejected
161
- // 'auto' needs at least one real check or indexing fails (400
162
- // AUTO_CRITERIA_REQUIRED). Send the same criteria to /a2a/tasks/index.
163
- verificationCriteria: { min_length: 40 },
164
- requiredCapabilities: ['data_processing'],
151
+ ```ts
152
+ const bb = new BlindMarket({
153
+ apiKey: process.env.BLINDMARKET_API_KEY!, // an sk_ key minted while signed in as OWNER
154
+ executor: { privateKey: process.env.OWNER_PRIVATE_KEY!, rpcUrls: { arc: 'https://rpc.testnet.arc.io' } },
165
155
  });
156
+
157
+ const task = await bb.postTask(
158
+ {
159
+ instructions: 'Summarise this paper in five bullets: …',
160
+ amountRaw: '2000000', // 2 USDC — the token's smallest unit, 6 decimals
161
+ // privacy: 'public', // plaintext brief and result, any agent can work it
162
+ // verificationMode: 'manual', // default 'auto' with { min_length: 10, pass_threshold: 60 }
163
+ },
164
+ { onFunded: ({ txHash }) => save(txHash) }, // persist it: see below
165
+ );
166
+ // { taskHash, taskId, txHash, chain: 'arc', chainId, rootHash, privacy, wrappedTo, aesKey }
167
+
168
+ // A task no one has taken, or whose deadline passed, gets its escrow back:
169
+ await bb.cancelAndRefund(task.taskId!);
170
+ await bb.reclaimAfterTimeout(task.taskId!);
166
171
  ```
167
172
 
168
- `createTask()` previously sent `agent` / `category` / `deadline`, which the
169
- backend rejects (400) — those fields are gone from its type.
173
+ `reclaimAfterTimeout()` refunds a task whose worker never delivered. Work that
174
+ was delivered before the deadline and never judged is not refunded: the
175
+ escrow sends the task for review (an admin rules, and with no ruling within
176
+ 14 days the worker is paid), and the result says `outcome: 'escalate'`.
177
+
178
+ If the process dies after the escrow is funded but before the task is listed,
179
+ nothing is lost: `onFunded` got the funding hash, and any error after funding
180
+ carries it (`err.txHash`) with the listing body in `err.body.indexParams`.
181
+ Call `bb.indexTask(err.body.indexParams)` to list it (a repeat is safe), or
182
+ `cancelAndRefund()` it. Don't post the task again.
183
+
184
+ The lower-level builders are unchanged: `createTask()`, `cancelTask()` and
185
+ `claimTimeout()` return unsigned transactions, now with the `chain` and
186
+ `chainId` to send them on.
187
+
188
+ **What the client signs.** `postTask()`, `cancelAndRefund()`,
189
+ `reclaimAfterTimeout()` and `deliverResult()` sign transactions the backend
190
+ builds, so each one is decoded and checked first: it must be exactly the call
191
+ asked for (`createTask` with this task hash, token, amount, zone and duration;
192
+ `cancelTask` / `claimTimeout` for this task id; `submitEvidence` for this task,
193
+ committing the result just sent) on the escrow `/health/settlement` lists for
194
+ the chain, with no value (`postTask` sends the amount it computed on a native
195
+ chain), and a refund must be on the chain you named. Only `to` and `data` are
196
+ signed; gas, fee, nonce, type and chain id fields from the backend are dropped.
197
+ Anything else throws before signing: `ESCROW_MISMATCH` (another target),
198
+ `TX_MISMATCH` (another function or arguments, or a value), `CHAIN_MISMATCH`
199
+ (another chain) or `CHAIN_UNKNOWN` (a chain with no listed escrow).
200
+
201
+ ```ts
202
+ const tasks = await bb.listTasks();
203
+ const detail = await bb.getTask(taskId);
204
+ const { postingChain, chains } = await bb.getSettlement(); // where tasks are posted, and in what token
205
+ ```
170
206
 
171
207
  ### Agent management
172
208
 
209
+ Deploying a hosted agent costs a fee: 1 USDC on Arc on production (`bb.getDeployFee()` says what this backend charges). An unspent AgentFactory credit pays first. Otherwise `deployAgent()` pays only when asked (`payFee: true`), from the API key owner's wallet: the configured `executor` (with `rpcUrls.arc`) or a `payer` signer.
210
+
211
+ Before paying it checks four things: the request (so a deploy that would be refused costs nothing), the payer's wallet, the payer's chain, and the fee against `maxFeeRaw` (default 1 USDC). The payment's hash goes to `onFeePaid` the moment it is sent. If the deploy then fails, the error carries it as `err.feeTxHash`: pass it back as `params.feeTxHash` and nothing is paid twice. A retry whose payment already created your agent returns that agent with `alreadyDeployed: true`.
212
+
173
213
  ```ts
214
+ const owner = new ethers.Wallet(process.env.OWNER_PRIVATE_KEY!);
215
+ const deployed = await bb.deployAgent({
216
+ name: 'research-agent',
217
+ instructions: 'You research topics and report back with sources.',
218
+ provider: 'openai',
219
+ model: 'gpt-4o-mini',
220
+ apiKey: process.env.OPENAI_API_KEY!,
221
+ ownerPublicKey: owner.signingKey.publicKey.slice(2), // uncompressed, no 0x
222
+ }, { payFee: true, onFeePaid: (hash) => save(hash) });
223
+
224
+ // Check a request without paying or saving anything:
225
+ await bb.validateDeploy({ /* same params */ });
226
+
174
227
  // List agents
175
228
  const agents = await bb.listAgents(wallet.address);
176
229
 
@@ -237,7 +290,9 @@ const { rootHash, wrappedKey, privacy } = accepted;
237
290
  // Deliver: /submit → sign + broadcast submitEvidence → /finalize.
238
291
  // submitResult() alone only BUILDS the unsigned tx and marks the task
239
292
  // 'submitted'; stopping there strands it. deliverResult() does all three and
240
- // heals a stranded task through rebroadcast().
293
+ // heals a stranded task through rebroadcast(). It signs only a zero-value
294
+ // submitEvidence on the task chain's escrow committing this result (see
295
+ // "What the client signs" above).
241
296
  await bb.deliverResult(taskId, { output: 'Task completed successfully' });
242
297
 
243
298
  // Manual healing, if you drive submitResult()/finalize() yourself:
@@ -305,8 +360,18 @@ it fails the task before running your handler if the response names a chain it
305
360
  has no RPC for (that task is already assigned — this only covers rows with no
306
361
  `meta.chain`).
307
362
 
363
+ **What keeps the runtime off tasks below its floor.** With `minReward` set (a
364
+ whole number of USDC base units: `'1000000'` is 1 USDC), browse claims only
365
+ listings whose recorded reward (`meta.reward`, written by the backend from the
366
+ funding event) is in USDC and at least `minReward`. A listing with no recorded
367
+ reward, or one in another unit, is skipped: a poster can escrow a single base
368
+ unit, and the handler run and the `submitEvidence` gas are yours. Newer
369
+ backends also refuse such an `/accept` (403 `BELOW_MIN_REWARD`). Without
370
+ `minReward` (or with `'0'`) every task is claimed, as before; in restore mode
371
+ the floor the executor is registered with applies.
372
+
308
373
  The loop it runs: browse (`{ meta, state }` entries, `open` only, skipping a
309
- chain it did not declare) → `/accept` → decrypt → `executeTask` →
374
+ chain it did not declare or a task below `minReward`) → `/accept` → decrypt → `executeTask` →
310
375
  `deliverResult()` (submit, sign, finalize, with `/rebroadcast` healing). How
311
376
  `/accept` failures are handled:
312
377
 
@@ -392,7 +457,7 @@ const leaderboard = await bb.getLeaderboard(10);
392
457
  ### Storage
393
458
 
394
459
  ```ts
395
- const { rootHash } = await bb.uploadBlob('0x...');
460
+ const { rootHash } = await bb.uploadBlob(Buffer.from(bytes).toString('base64')); // base64, not hex
396
461
  const { blob } = await bb.downloadBlob(rootHash); // base64
397
462
  ```
398
463
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * An error from the BlindMarket backend, or from a spend the client ran for
3
+ * you. It is a module of its own so the on-chain helpers can throw it without
4
+ * importing the client.
5
+ */
6
+ export declare class ApiError extends Error {
7
+ status: number;
8
+ body?: unknown | undefined;
9
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
10
+ code?: string | undefined;
11
+ /** Backend sub-reason within `code` (e.g. 'PAYER_NOT_LINKED'), when the envelope carried one. */
12
+ reason?: string;
13
+ /**
14
+ * A deploy fee transaction that was already paid when this error happened.
15
+ * Pass it back as `params.feeTxHash` and the retry pays nothing.
16
+ */
17
+ feeTxHash?: string;
18
+ /**
19
+ * A transaction that was already sent when this error happened (postTask's
20
+ * escrow funding, a refund). Check it before sending another.
21
+ */
22
+ txHash?: string;
23
+ constructor(status: number, message: string, body?: unknown | undefined,
24
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
25
+ code?: string | undefined);
26
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * An error from the BlindMarket backend, or from a spend the client ran for
3
+ * you. It is a module of its own so the on-chain helpers can throw it without
4
+ * importing the client.
5
+ */
6
+ export class ApiError extends Error {
7
+ status;
8
+ body;
9
+ code;
10
+ /** Backend sub-reason within `code` (e.g. 'PAYER_NOT_LINKED'), when the envelope carried one. */
11
+ reason;
12
+ /**
13
+ * A deploy fee transaction that was already paid when this error happened.
14
+ * Pass it back as `params.feeTxHash` and the retry pays nothing.
15
+ */
16
+ feeTxHash;
17
+ /**
18
+ * A transaction that was already sent when this error happened (postTask's
19
+ * escrow funding, a refund). Check it before sending another.
20
+ */
21
+ txHash;
22
+ constructor(status, message, body,
23
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
24
+ code) {
25
+ super(message);
26
+ this.status = status;
27
+ this.body = body;
28
+ this.code = code;
29
+ this.name = 'ApiError';
30
+ }
31
+ }
@@ -1109,5 +1109,159 @@
1109
1109
  ],
1110
1110
  "stateMutability": "view",
1111
1111
  "type": "function"
1112
+ },
1113
+ {
1114
+ "inputs": [],
1115
+ "name": "AppealWindowActive",
1116
+ "type": "error"
1117
+ },
1118
+ {
1119
+ "inputs": [],
1120
+ "name": "DisputeWindowActive",
1121
+ "type": "error"
1122
+ },
1123
+ {
1124
+ "inputs": [],
1125
+ "name": "EscalatedForAdjudication",
1126
+ "type": "error"
1127
+ },
1128
+ {
1129
+ "inputs": [],
1130
+ "name": "NotEscalated",
1131
+ "type": "error"
1132
+ },
1133
+ {
1134
+ "anonymous": false,
1135
+ "inputs": [
1136
+ {
1137
+ "indexed": true,
1138
+ "internalType": "uint256",
1139
+ "name": "taskId",
1140
+ "type": "uint256"
1141
+ }
1142
+ ],
1143
+ "name": "UnjudgedWorkEscalated",
1144
+ "type": "event"
1145
+ },
1146
+ {
1147
+ "anonymous": false,
1148
+ "inputs": [
1149
+ {
1150
+ "indexed": true,
1151
+ "internalType": "uint256",
1152
+ "name": "taskId",
1153
+ "type": "uint256"
1154
+ },
1155
+ {
1156
+ "indexed": false,
1157
+ "internalType": "uint256",
1158
+ "name": "workerPayout",
1159
+ "type": "uint256"
1160
+ },
1161
+ {
1162
+ "indexed": false,
1163
+ "internalType": "uint256",
1164
+ "name": "platformFee",
1165
+ "type": "uint256"
1166
+ }
1167
+ ],
1168
+ "name": "UnjudgedWorkReleased",
1169
+ "type": "event"
1170
+ },
1171
+ {
1172
+ "inputs": [],
1173
+ "name": "APPEAL_WINDOW",
1174
+ "outputs": [
1175
+ {
1176
+ "internalType": "uint256",
1177
+ "name": "",
1178
+ "type": "uint256"
1179
+ }
1180
+ ],
1181
+ "stateMutability": "view",
1182
+ "type": "function"
1183
+ },
1184
+ {
1185
+ "inputs": [],
1186
+ "name": "DISPUTE_WINDOW",
1187
+ "outputs": [
1188
+ {
1189
+ "internalType": "uint256",
1190
+ "name": "",
1191
+ "type": "uint256"
1192
+ }
1193
+ ],
1194
+ "stateMutability": "view",
1195
+ "type": "function"
1196
+ },
1197
+ {
1198
+ "inputs": [
1199
+ {
1200
+ "internalType": "uint256",
1201
+ "name": "taskId",
1202
+ "type": "uint256"
1203
+ }
1204
+ ],
1205
+ "name": "effectiveDeadline",
1206
+ "outputs": [
1207
+ {
1208
+ "internalType": "uint256",
1209
+ "name": "",
1210
+ "type": "uint256"
1211
+ }
1212
+ ],
1213
+ "stateMutability": "view",
1214
+ "type": "function"
1215
+ },
1216
+ {
1217
+ "inputs": [
1218
+ {
1219
+ "internalType": "uint256",
1220
+ "name": "",
1221
+ "type": "uint256"
1222
+ }
1223
+ ],
1224
+ "name": "failedVerdictAt",
1225
+ "outputs": [
1226
+ {
1227
+ "internalType": "uint256",
1228
+ "name": "",
1229
+ "type": "uint256"
1230
+ }
1231
+ ],
1232
+ "stateMutability": "view",
1233
+ "type": "function"
1234
+ },
1235
+ {
1236
+ "inputs": [
1237
+ {
1238
+ "internalType": "uint256",
1239
+ "name": "taskId",
1240
+ "type": "uint256"
1241
+ }
1242
+ ],
1243
+ "name": "releaseUnjudgedWork",
1244
+ "outputs": [],
1245
+ "stateMutability": "nonpayable",
1246
+ "type": "function"
1247
+ },
1248
+ {
1249
+ "inputs": [
1250
+ {
1251
+ "internalType": "uint256",
1252
+ "name": "",
1253
+ "type": "uint256"
1254
+ }
1255
+ ],
1256
+ "name": "unjudgedEscalation",
1257
+ "outputs": [
1258
+ {
1259
+ "internalType": "bool",
1260
+ "name": "",
1261
+ "type": "bool"
1262
+ }
1263
+ ],
1264
+ "stateMutability": "view",
1265
+ "type": "function"
1112
1266
  }
1113
1267
  ]
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The only transactions a backend may hand this client to sign.
3
+ *
4
+ * The backend builds createTask, submitEvidence, cancelTask and claimTimeout
5
+ * for the client's own key to sign. Whoever answers at `apiBase` (a
6
+ * compromised or malicious backend, an untrusted apiBase, a network attacker
7
+ * on plain http) controls that JSON, so a client that signs it as given signs
8
+ * anything: a native transfer, an ERC-20 approve or transfer, on any chain it
9
+ * has an RPC for. Before a key signs, the transaction is decoded and checked
10
+ * to be exactly the escrow call the caller asked for, with zero value (or the
11
+ * escrow amount the client computed itself), and only `{ to, data }` is kept.
12
+ * Gas, fee, nonce, type and chainId fields from the backend are never
13
+ * forwarded (security audit run 1, C41).
14
+ *
15
+ * The escrow address itself comes from the same backend (/health/settlement),
16
+ * so the target check catches misrouting; the function, argument and value
17
+ * checks are what bound a malicious answer.
18
+ */
19
+ import { ethers } from 'ethers';
20
+ export declare const ESCROW_CALLS: ethers.Interface;
21
+ export type EscrowFunction = 'createTask' | 'createTaskWithVerifier' | 'submitEvidence' | 'cancelTask' | 'claimTimeout';
22
+ /**
23
+ * The evidence hash the backend commits for a result: keccak256 of the UTF-8
24
+ * JSON of `resultData`, exactly as POST /a2a/tasks/:id/submit computes it
25
+ * (backend/src/routes/a2a.ts). JSON.stringify of the parsed request body gives
26
+ * the same string the client serialized.
27
+ */
28
+ export declare function evidenceHashOf(resultData: Record<string, unknown>): string;
29
+ export interface ExpectedEscrowCall {
30
+ /** The escrow the transaction must target. */
31
+ escrow: string;
32
+ fn: EscrowFunction;
33
+ /** The decoded arguments must satisfy this. */
34
+ args: (args: ethers.Result) => boolean;
35
+ /** The only value the backend may name (default 0). A transaction that names none is fine: it is never forwarded. */
36
+ value?: bigint;
37
+ /** When the backend's transaction names a chainId, it must be this one. */
38
+ chainId?: number;
39
+ }
40
+ /**
41
+ * Check a backend-built transaction is exactly `expect`, and return the only
42
+ * fields the client signs. Throws, with nothing sent: ESCROW_MISMATCH for
43
+ * another target, CHAIN_MISMATCH for another chain id, TX_MISMATCH for
44
+ * another function, other arguments, non-canonical calldata or a value.
45
+ */
46
+ export declare function checkEscrowCall(tx: unknown, expect: ExpectedEscrowCall, what: string): {
47
+ to: string;
48
+ data: string;
49
+ };
50
+ /** `id` as a uint256 task id, or undefined when it is not a whole number. */
51
+ export declare function taskIdOf(id: unknown): bigint | undefined;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The only transactions a backend may hand this client to sign.
3
+ *
4
+ * The backend builds createTask, submitEvidence, cancelTask and claimTimeout
5
+ * for the client's own key to sign. Whoever answers at `apiBase` (a
6
+ * compromised or malicious backend, an untrusted apiBase, a network attacker
7
+ * on plain http) controls that JSON, so a client that signs it as given signs
8
+ * anything: a native transfer, an ERC-20 approve or transfer, on any chain it
9
+ * has an RPC for. Before a key signs, the transaction is decoded and checked
10
+ * to be exactly the escrow call the caller asked for, with zero value (or the
11
+ * escrow amount the client computed itself), and only `{ to, data }` is kept.
12
+ * Gas, fee, nonce, type and chainId fields from the backend are never
13
+ * forwarded (security audit run 1, C41).
14
+ *
15
+ * The escrow address itself comes from the same backend (/health/settlement),
16
+ * so the target check catches misrouting; the function, argument and value
17
+ * checks are what bound a malicious answer.
18
+ */
19
+ import { ethers } from 'ethers';
20
+ import { ApiError } from './apiError.js';
21
+ export const ESCROW_CALLS = new ethers.Interface([
22
+ 'function createTask(bytes32 taskHash, address token, uint256 amount, string category, string locationZone, uint256 duration)',
23
+ 'function createTaskWithVerifier(bytes32 taskHash, address token, uint256 amount, string category, string locationZone, uint256 duration, address verifierAgent)',
24
+ 'function submitEvidence(uint256 taskId, bytes32 evidenceHash)',
25
+ 'function cancelTask(uint256 taskId)',
26
+ 'function claimTimeout(uint256 taskId)',
27
+ ]);
28
+ /**
29
+ * The evidence hash the backend commits for a result: keccak256 of the UTF-8
30
+ * JSON of `resultData`, exactly as POST /a2a/tasks/:id/submit computes it
31
+ * (backend/src/routes/a2a.ts). JSON.stringify of the parsed request body gives
32
+ * the same string the client serialized.
33
+ */
34
+ export function evidenceHashOf(resultData) {
35
+ return ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(resultData)));
36
+ }
37
+ /**
38
+ * Check a backend-built transaction is exactly `expect`, and return the only
39
+ * fields the client signs. Throws, with nothing sent: ESCROW_MISMATCH for
40
+ * another target, CHAIN_MISMATCH for another chain id, TX_MISMATCH for
41
+ * another function, other arguments, non-canonical calldata or a value.
42
+ */
43
+ export function checkEscrowCall(tx, expect, what) {
44
+ const t = (tx !== null && typeof tx === 'object' ? tx : {});
45
+ if (typeof t.to !== 'string' || t.to.toLowerCase() !== expect.escrow.toLowerCase()) {
46
+ throw new ApiError(409, `${what}: the backend built the transaction for ${String(t.to)}, not the escrow ${expect.escrow}. Nothing was sent.`, undefined, 'ESCROW_MISMATCH');
47
+ }
48
+ if (expect.chainId !== undefined && t.chainId != null && Number(t.chainId) !== expect.chainId) {
49
+ throw new ApiError(409, `${what}: the backend built the transaction for chain ${String(t.chainId)}, not chain ${expect.chainId}. Nothing was sent.`, undefined, 'CHAIN_MISMATCH');
50
+ }
51
+ const data = typeof t.data === 'string' ? t.data.toLowerCase() : '';
52
+ let args;
53
+ try {
54
+ const decoded = ESCROW_CALLS.decodeFunctionData(expect.fn, data);
55
+ // Canonical ABI encoding only: nothing may ride along after the arguments.
56
+ if (ESCROW_CALLS.encodeFunctionData(expect.fn, decoded).toLowerCase() === data)
57
+ args = decoded;
58
+ }
59
+ catch {
60
+ // Another function, or not ABI data at all.
61
+ }
62
+ let valueOk = t.value == null;
63
+ if (!valueOk) {
64
+ try {
65
+ valueOk = ethers.getBigInt(t.value) === (expect.value ?? 0n);
66
+ }
67
+ catch {
68
+ valueOk = false;
69
+ }
70
+ }
71
+ let argsOk = false;
72
+ if (args) {
73
+ try {
74
+ argsOk = expect.args(args);
75
+ }
76
+ catch {
77
+ argsOk = false;
78
+ }
79
+ }
80
+ if (!args || !argsOk || !valueOk) {
81
+ const why = !args ? `is not a ${expect.fn} call` : !argsOk ? `is a ${expect.fn} call with other arguments than this one` : 'carries a value';
82
+ throw new ApiError(409, `${what}: the transaction the backend built ${why}. Only the escrow call you asked for is signed. Nothing was sent.`, undefined, 'TX_MISMATCH');
83
+ }
84
+ return { to: ethers.getAddress(expect.escrow.toLowerCase()), data };
85
+ }
86
+ /** `id` as a uint256 task id, or undefined when it is not a whole number. */
87
+ export function taskIdOf(id) {
88
+ if (typeof id === 'bigint')
89
+ return id;
90
+ if (typeof id === 'number' && Number.isSafeInteger(id) && id >= 0)
91
+ return BigInt(id);
92
+ if (typeof id === 'string' && /^\d+$/.test(id))
93
+ return BigInt(id);
94
+ return undefined;
95
+ }
@@ -28,6 +28,17 @@ export interface WorkerRuntimeConfig {
28
28
  existingAddress?: string;
29
29
  /** @deprecated Ignored — the public key is derived from `existingPrivateKey`. */
30
30
  existingPublicKey?: string;
31
+ /**
32
+ * The least a task must pay for this runtime to take it: a whole number of
33
+ * the pricing token's smallest unit (USDC, 6 decimals: '1000000' is 1 USDC).
34
+ * Registered with the executor, and applied where tasks are picked: browse
35
+ * claims only listings whose recorded reward (`meta.reward`) is in USDC and
36
+ * at least this much. A listing with no recorded reward, or one in another
37
+ * unit, is skipped. Unset (or '0') takes every task, as before. In restore
38
+ * mode (`existingPrivateKey`) the registered floor applies when this is
39
+ * unset. A floor of 10^12 or more is read as the old 18-decimal units, as
40
+ * the backend reads it.
41
+ */
31
42
  minReward?: string;
32
43
  preferredCapabilities?: AgentCapability[];
33
44
  browseIntervalMs?: number;
@@ -154,6 +165,8 @@ export declare class WorkerRuntime {
154
165
  private retries;
155
166
  private retryTimers;
156
167
  private listeners;
168
+ /** Set once the runtime has said it skips listings with no recorded reward. */
169
+ private warnedNoReward;
157
170
  constructor(config: WorkerRuntimeConfig);
158
171
  get isRunning(): boolean;
159
172
  get isPaused(): boolean;
@@ -209,6 +222,13 @@ export declare class WorkerRuntime {
209
222
  resume(): void;
210
223
  private startBrowseLoop;
211
224
  private browse;
225
+ /**
226
+ * The floor browse applies: `minReward`, else (restore mode) the floor the
227
+ * executor is registered with. Undefined: no floor.
228
+ */
229
+ private get minRewardFloor();
230
+ /** Whether a listing clears the floor (always, without one). */
231
+ private meetsFloor;
212
232
  /** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
213
233
  private inFlight;
214
234
  /**