@blindmarket/sdk 0.6.1 → 0.6.4

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 ADDED
@@ -0,0 +1,132 @@
1
+ # Changelog — @blindmarket/sdk
2
+
3
+ This package is 0.x: a minor version may contain breaking changes. They are
4
+ listed here with how to migrate.
5
+
6
+ ## 0.6.4
7
+
8
+ ### Fixes
9
+
10
+ **`WorkerRuntime` can take tasks on Arc.** Production posts new tasks on Arc
11
+ (Arc Testnet, chain 5042002) since backend #73, but `SETTLEMENT_CHAINS` was
12
+ `['0g', 'base']`, so a runtime never declared Arc and threw on any task whose
13
+ `chain` was `'arc'`. `SETTLEMENT_CHAINS` is now `['0g', 'base', 'arc']` and
14
+ `A2APublicTaskMeta.chain` includes `'arc'`. To claim Arc tasks set
15
+ `rpcUrls.arc`; a runtime without it keeps declaring only the chains it has an
16
+ RPC for, and says so at start. The README examples and the "no RPC
17
+ configured" error now name `rpcUrls.arc` first.
18
+
19
+ ## 0.6.3
20
+
21
+ ### Fixes
22
+
23
+ **A scheduled NEEDS_WRAP re-try is no longer refused by its own back-off.**
24
+ `WorkerRuntime` re-tries a task that is waiting for its key to be wrapped on a
25
+ timer. A timer can fire up to a millisecond before `Date.now()` reaches the
26
+ back-off it was set for, and the re-try then counted as too early: nothing
27
+ re-tried the task until the next browse (by default up to 15 s later). Nothing was lost,
28
+ it was only late. No API change.
29
+
30
+ ### Documentation
31
+
32
+ The `supportedChains` doc comments (`RegisterExecutorInput`,
33
+ `ExecutorProfile`, `WorkerRuntime`, the `register_as_executor` tool, README) now
34
+ say what newer backends do with the list: they leave the executor out of
35
+ offers and refuse bids and `/accept` (409 `CHAIN_UNSUPPORTED`) for tasks on
36
+ chains it did not declare. Older backends only store it. No backend filters
37
+ browse results by it, so `WorkerRuntime` still checks a task's chain itself.
38
+
39
+ ## 0.6.0
40
+
41
+ ### Breaking changes
42
+
43
+ **`createTask(params)` takes `CreateTaskRequest`.** The old shape
44
+ (`agent` / `category` / `deadline`) was rejected by the backend with 400, so it
45
+ never worked. `taskHash` and `duration` are now required; `agent`, `category`
46
+ and `deadline` are gone.
47
+
48
+ ```ts
49
+ // before (always 400)
50
+ await bb.createTask({ agent, amount, token, category, locationZone, deadline });
51
+ // after
52
+ await bb.createTask({
53
+ taskHash, // bytes32: sha256 of the encrypted brief
54
+ token, amount, locationZone,
55
+ duration: '86400', // seconds, as a string; deadline = now + duration
56
+ targetExecutorType: 'agent',
57
+ verificationMode: 'auto',
58
+ verificationCriteria: { min_length: 40 },
59
+ });
60
+ ```
61
+
62
+ **`browseA2ATasks()` and `getPostedTasks()` return `{ tasks: A2ATaskEntry[]; total? }`.**
63
+ Entries are `{ meta, state }`, which is what the backend has always sent; the
64
+ old `A2ATaskState[]` type made `task.taskId` / `task.status` read `undefined`.
65
+
66
+ ```ts
67
+ // before // after
68
+ tasks[0].taskId tasks[0].state.taskId
69
+ tasks[0].status tasks[0].state.status
70
+ tasks[0].meta.chain // settlement chain
71
+ ```
72
+
73
+ **`getExecutions()` returns `{ executions: A2ATaskEntry[]; total }`**, not
74
+ `{ tasks }`. Rename the destructured field and read ids from `entry.state`.
75
+
76
+ **`createBlindMarketTools(bb)` / `tools(bb)` omit `submit_result` unless the
77
+ client has a signer.** The tool now performs the whole delivery (submit → sign
78
+ `submitEvidence` → finalize); without a key it could only strand tasks.
79
+ Construct the client with `new BlindMarket({ apiKey, executor: { privateKey, rpcUrls } })`
80
+ to get it back. When it is omitted a one-time `console.warn` says so.
81
+ `createA2ATools()` / `createTaskTools()` are affected the same way.
82
+
83
+ **`WorkerRuntime` requires a key.** `start()` throws unless `privateKey` (the
84
+ wallet that owns the API key) or `existingPrivateKey` is set. A keyless runtime
85
+ used to register a random wallet's public key over the owner's on every start,
86
+ accept tasks (assigned on-chain, irrevocably) and then fail to sign their
87
+ delivery. To only read tasks, call `bb.browseA2ATasks()`.
88
+
89
+ **`WorkerRuntime` has no default RPC.** `rpcUrl` used to default to 0G
90
+ *testnet* while `apiBase` defaults to *production*. `start()` now throws unless
91
+ `rpcUrl` (0G) and/or `rpcUrls.base` is set; pass the RPC of the network your
92
+ backend settles on. `rpcUrl` is 0G only and never stands in for Base.
93
+
94
+ **`WorkerRuntime` restore mode checks the key.** With `existingPrivateKey`,
95
+ `start()` throws if the key's address is not the executor the API key resolves
96
+ to. `existingAddress` is now an optional cross-check and `existingPublicKey` is
97
+ ignored (both are derived from the key), so `existingPrivateKey` alone is enough.
98
+
99
+ **`createAgent({ privateKey })` verifies ownership before registering.** It
100
+ calls the new `bb.whoami()` (`GET /api/v1/api-keys/whoami`) and throws
101
+ `ApiError` 409 `OWNER_MISMATCH` without touching `/register` when the key is
102
+ not the API key's owner. Previously the check ran after `/register` had already
103
+ replaced the owner's public key. A legacy shared `AGENT_API_KEY` (principal
104
+ `"agent"`, not a wallet) is refused the same way. On a backend without the
105
+ whoami route the old after-the-fact check still runs.
106
+
107
+ ### Added
108
+
109
+ - `deliverResult()`, `rebroadcast()`, `BlindMarketConfig.executor`, `bb.canSign`.
110
+ - `bb.whoami()`.
111
+ - `supportedChains` on `registerExecutor()` / `createAgent()`, and
112
+ `WorkerRuntime.declaredChains`. **It is a declaration only**: the backend
113
+ stores it and does not filter offers or `/accept` by it. `WorkerRuntime`
114
+ enforces it client-side (browse skips other chains; a post-accept check
115
+ fails the task before the handler runs).
116
+ - `WorkerRuntimeConfig.assignmentPendingTimeoutMs` (default 3 min).
117
+ - `ApiError.code` carries the backend error code.
118
+
119
+ ### Fixed — `WorkerRuntime` accept handling
120
+
121
+ - `503 ASSIGNMENT_PENDING` is re-tried (the backend keeps the task for the
122
+ caller) instead of marking the execution failed and holding the task forever.
123
+ If it never confirms, the slot is freed and the accept is re-tried from the
124
+ browse loop with back-off, at most 6 rounds.
125
+ - `503 REWRAP_FAILED` / `SETTLEMENT_FAILED` (the backend released the task)
126
+ no longer leave a dead entry in `executions`; the task can be claimed again
127
+ after a per-task exponential back-off.
128
+ - `403 NEEDS_WRAP` no longer holds a concurrency slot for `wrapTimeoutMs`.
129
+ After a timeout the task is backed off exponentially instead of being picked
130
+ up again by the next browse, which let three unwrappable tasks starve a
131
+ runtime indefinitely. A brief sealed to a rotated custody key is skipped at
132
+ once.
package/README.md CHANGED
@@ -8,6 +8,10 @@ TypeScript SDK for [BlindMarket](https://github.com/JemIIahh/BlindMarket) — th
8
8
  npm install @blindmarket/sdk
9
9
  ```
10
10
 
11
+ > **Upgrading from 0.5.x?** 0.6.0 changes several types and refuses
12
+ > configurations that used to strand tasks. See [CHANGELOG.md](./CHANGELOG.md)
13
+ > for the breaking changes and how to migrate.
14
+
11
15
  ## Quick Start
12
16
 
13
17
  ```ts
@@ -21,8 +25,12 @@ const bb = new BlindMarket({
21
25
  const health = await bb.health();
22
26
  console.log('Status:', health.status);
23
27
 
24
- // Register as an A2A executor (generates wallet + registers in one call)
25
- const { executor, wallet } = await bb.createAgent({
28
+ // Register as an A2A executor. The executor is ALWAYS the wallet that owns the
29
+ // API key (the backend takes the address from auth), so pass that wallet's key:
30
+ // its public half is what briefs get wrapped to, and it signs submitEvidence.
31
+ // The key never leaves this process.
32
+ const { executor } = await bb.createAgent({
33
+ privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
26
34
  displayName: 'DataBot',
27
35
  capabilities: [
28
36
  AgentCap.DATA_PROCESSING,
@@ -32,10 +40,21 @@ const { executor, wallet } = await bb.createAgent({
32
40
  minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
33
41
  });
34
42
 
35
- console.log('Executor:', executor.address);
36
- console.log('Private key (store securely):', wallet.privateKey);
43
+ console.log('Executor:', executor.address); // === the API key owner's address
37
44
  ```
38
45
 
46
+ > **`privateKey` is new and recommended.** The backend registers the API key's
47
+ > owner as the executor — never an address from the request — and builds
48
+ > `submitEvidence` for that address. Pass the owner wallet's `privateKey` (or
49
+ > `new BlindMarket({ apiKey, executor: { privateKey, rpcUrls } })`): its
50
+ > uncompressed public key is registered. `createAgent()` first asks the backend
51
+ > who owns the API key (`bb.whoami()` → `GET /api/v1/api-keys/whoami`) and
52
+ > throws `409 OWNER_MISMATCH` **before registering anything** if the key is not
53
+ > the owner's. Without a key `createAgent()` still generates a random wallet
54
+ > and returns its private key once, as before — that wallet can decrypt briefs
55
+ > but cannot sign `submitEvidence` for the owner, and registering it replaces
56
+ > the owner's public key. `WorkerRuntime` refuses to run that way.
57
+
39
58
  ## Features
40
59
 
41
60
  - **Full REST API client** — task lifecycle, agent management, A2A, marketplace, messages, reputation
@@ -111,6 +130,13 @@ const response = await anthropic.messages.create({
111
130
  });
112
131
  ```
113
132
 
133
+ > **Tools that sign:** `submit_result` completes the whole delivery (submit →
134
+ > sign `submitEvidence` → finalize), so it is only offered when the client was
135
+ > built with `executor: { privateKey, rpcUrls }` — without it the tool is left
136
+ > out of `tools(bb)` / `createBlindMarketTools(bb)` and a one-time
137
+ > `console.warn` says so. `create_agent` reads the same config. Keys are never
138
+ > tool arguments and are never returned to the model.
139
+
114
140
  ## Usage
115
141
 
116
142
  ### Task lifecycle
@@ -122,17 +148,26 @@ const tasks = await bb.listTasks();
122
148
  // Get task details (includes A2A state + verification result)
123
149
  const task = await bb.getTask(taskId);
124
150
 
125
- // Build unsigned createTask tx (sign & broadcast with your wallet)
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.
126
153
  const { unsignedTx } = await bb.createTask({
127
- agent: wallet.address,
128
- amount: '100',
129
- token: '0x317227efcA18D004E12CA8046AEf7E1597458F25',
130
- category: 'photography',
131
- locationZone: 'nyc',
132
- deadline: Math.floor(Date.now() / 1000) + 86400,
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'],
133
165
  });
134
166
  ```
135
167
 
168
+ `createTask()` previously sent `agent` / `category` / `deadline`, which the
169
+ backend rejects (400) — those fields are gone from its type.
170
+
136
171
  ### Agent management
137
172
 
138
173
  ```ts
@@ -158,43 +193,64 @@ await bb.updateAgent(agentId, {
158
193
  ### A2A (agent-to-agent task execution)
159
194
 
160
195
  ```ts
161
- // Register as an executor with your own ethers Wallet
162
- const wallet = ethers.Wallet.createRandom();
196
+ const bb = new BlindMarket({
197
+ apiKey,
198
+ // Optional: the API key owner's wallet + an RPC per chain your tasks settle
199
+ // on. Enables deliverResult() and the submit_result tool.
200
+ executor: { privateKey, rpcUrls: { arc: 'https://rpc.testnet.arc.io', base: 'https://sepolia.base.org' } },
201
+ });
202
+
203
+ // Register as an executor. The executor ADDRESS is always the API key's owner
204
+ // (any `address` sent is ignored); the public key is what briefs get wrapped to.
205
+ const wallet = new ethers.Wallet(privateKey);
163
206
  await bb.registerExecutor({
164
- address: wallet.address,
165
207
  displayName: 'my-agent',
166
208
  capabilities: ['data_processing', 'web_research'],
167
209
  // Uncompressed, no 0x. `wallet.publicKey` is the compressed key, which is rejected.
168
210
  publicKey: wallet.signingKey.publicKey.slice(2),
169
- // Chains you can sign submitEvidence on (optional; defaults to 0g and base)
170
- supportedChains: ['0g', 'base'],
211
+ // Chains you can sign submitEvidence on (optional). Older backends only
212
+ // store it; newer ones also leave you out of offers and refuse /accept
213
+ // (409 CHAIN_UNSUPPORTED) on other chains. Neither filters browse results,
214
+ // so check entry.meta.chain before accepting (WorkerRuntime does).
215
+ supportedChains: ['arc', 'base'],
171
216
  });
172
217
 
173
- // Browse available tasks
218
+ // Browse available tasks — entries are { meta, state }
174
219
  const { tasks } = await bb.browseA2ATasks({
175
220
  capabilities: ['data_processing'],
176
221
  });
177
-
178
- // Bid and accept
179
- await bb.bidOnTask(taskId);
180
- const { task, wrappedKey } = await bb.acceptTask(taskId);
181
-
182
- // Submit result
183
- await bb.submitResult(taskId, {
184
- output: 'Task completed successfully',
185
- });
222
+ // An accept assigns on-chain and cannot be undone: only take a chain you have an RPC for.
223
+ const open = tasks.filter((t) => t.state.status === 'open' && t.meta.chain === 'base');
224
+ const taskId = open[0].state.taskId;
225
+
226
+ // Claim it. Nobody "assigns" you: /accept is the claim (and assigns on-chain).
227
+ // 403 NEEDS_WRAP = the brief key isn't wrapped to you yet: bid, then retry.
228
+ let accepted;
229
+ try {
230
+ accepted = await bb.acceptTask(taskId);
231
+ } catch (err) {
232
+ if (err.code !== 'NEEDS_WRAP') throw err;
233
+ await bb.bidOnTask(taskId); // then poll acceptTask() until the poster wraps
234
+ }
235
+ const { rootHash, wrappedKey, privacy } = accepted;
236
+
237
+ // Deliver: /submit → sign + broadcast submitEvidence → /finalize.
238
+ // submitResult() alone only BUILDS the unsigned tx and marks the task
239
+ // 'submitted'; stopping there strands it. deliverResult() does all three and
240
+ // heals a stranded task through rebroadcast().
241
+ await bb.deliverResult(taskId, { output: 'Task completed successfully' });
242
+
243
+ // Manual healing, if you drive submitResult()/finalize() yourself:
244
+ const { chain, unsignedSubmitEvidence } = await bb.rebroadcast(taskId);
186
245
 
187
246
  // Check posted/executed tasks
188
- const posted = await bb.getPostedTasks();
189
- const executed = await bb.getExecutions();
247
+ const { tasks: posted } = await bb.getPostedTasks();
248
+ const { executions } = await bb.getExecutions();
190
249
  ```
191
250
 
192
251
  ### Running a worker (`WorkerRuntime`)
193
252
 
194
- `WorkerRuntime` browses, accepts, executes and settles A2A tasks for you. A
195
- task is escrowed on exactly one chain and its `submitEvidence` must be signed
196
- on that chain, so the runtime **declares to the backend only the chains it has
197
- an RPC for** — that is what it gets offered:
253
+ `WorkerRuntime` browses, accepts, executes and settles A2A tasks for you.
198
254
 
199
255
  ```ts
200
256
  import { WorkerRuntime, AgentCap } from '@blindmarket/sdk';
@@ -203,21 +259,68 @@ const runtime = new WorkerRuntime({
203
259
  apiKey: process.env.BLINDMARKET_API_KEY!,
204
260
  displayName: 'my-worker',
205
261
  capabilities: [AgentCap.DATA_PROCESSING],
206
- // 0G RPC (this is the default). It is 0G only; it never stands in for Base.
207
- rpcUrl: 'https://evmrpc-testnet.0g.ai',
208
- // New tasks on production are posted on Base. Without this entry the
209
- // runtime declares 0G only and is not offered Base tasks.
210
- rpcUrls: { base: 'https://sepolia.base.org' },
262
+ // REQUIRED: the key of the wallet that owns the API key. The backend assigns
263
+ // accepted tasks on-chain to that wallet and builds submitEvidence for it, so
264
+ // it is the only key that can both decrypt briefs and settle. start() throws
265
+ // without a key, and throws — before registering anything — if the key is not
266
+ // the owner's.
267
+ privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
268
+ // REQUIRED: at least one RPC, on the network your `apiBase` settles on.
269
+ // There is NO default. Production posts new tasks on Arc (Arc Testnet,
270
+ // https://rpc.testnet.arc.io); without `rpcUrls.arc` the runtime skips them.
271
+ // `base` covers older Base Sepolia tasks. `rpcUrl` is the 0G RPC only and
272
+ // never stands in for another chain.
273
+ rpcUrls: { arc: process.env.ARC_RPC_URL!, base: process.env.BASE_RPC_URL! },
211
274
  executeTask: async ({ instructions }) => ({ output: await doTheWork(instructions) }),
212
275
  });
213
276
 
214
277
  await runtime.start(); // warns if a chain the SDK supports has no RPC configured
215
- console.log(runtime.declaredChains); // ['0g', 'base']
278
+ console.log(runtime.declaredChains); // ['base', 'arc']
216
279
  ```
217
280
 
218
- A runtime restored from a stored key re-registers only when its stored
281
+ **Key and RPC are mandatory.** Up to 0.5.x a runtime with no key registered a
282
+ random wallet's public key over the owner's on every `start()`, accepted tasks
283
+ (assigned on-chain, irrevocably) and then could not sign their delivery; and
284
+ `rpcUrl` defaulted to 0G *testnet* while `apiBase` defaults to *production*, so
285
+ a default runtime accepted mainnet tasks and failed ethers' chainId pin after
286
+ assignment. Both now fail at `start()`, before any request. To only look at
287
+ tasks, call `bb.browseA2ATasks()` — it needs neither. Use RPCs for the network
288
+ your backend settles on (testnet backend → testnet RPCs).
289
+
290
+ `existingPrivateKey` (instead of `privateKey`) restores a runtime without
291
+ re-registering: the stored profile is kept, and `start()` throws if the key is
292
+ not the executor the API key resolves to. It re-registers only when the stored
219
293
  `supportedChains` is unset or names a chain it has no RPC for; a narrower list
220
- you set deliberately (e.g. `['base']`) is kept.
294
+ you set deliberately (e.g. `['base']`) is kept. `existingAddress` is an optional
295
+ cross-check; `existingPublicKey` is ignored (derived from the key).
296
+
297
+ **What keeps the runtime off a chain it cannot settle.** A task is escrowed on
298
+ exactly one chain and `submitEvidence` must be signed there. The runtime
299
+ registers the chains it has an RPC for as `supportedChains`. Older backends
300
+ only store it; newer ones also keep other chains' tasks out of its offers and
301
+ refuse its `/accept` on them (409 `CHAIN_UNSUPPORTED`), but no backend filters
302
+ browse results by it. So the runtime enforces it itself, on every backend:
303
+ browse skips entries whose `meta.chain` it did not declare, and after `/accept`
304
+ it fails the task before running your handler if the response names a chain it
305
+ has no RPC for (that task is already assigned — this only covers rows with no
306
+ `meta.chain`).
307
+
308
+ The loop it runs: browse (`{ meta, state }` entries, `open` only, skipping a
309
+ chain it did not declare) → `/accept` → decrypt → `executeTask` →
310
+ `deliverResult()` (submit, sign, finalize, with `/rebroadcast` healing). How
311
+ `/accept` failures are handled:
312
+
313
+ | `/accept` answer | What the runtime does |
314
+ | --- | --- |
315
+ | `403 NEEDS_WRAP` | Bids once, then re-tries every `watchIntervalMs` **without holding a concurrency slot**. After `wrapTimeoutMs` (default 10 min) the task is skipped for `wrapTimeoutMs`, then 2×, 4× … (max 24 h), bidding again each round. |
316
+ | `403 NEEDS_WRAP`, "sealed to a rotated custody key" / "no public key" | The platform can never wrap it. Bids once (only the poster still can wrap) and goes straight to the long back-off. Detected from the message — the backend has no separate code. |
317
+ | `503 ASSIGNMENT_PENDING` | The assign tx is unconfirmed and the task stays yours: re-tries `/accept` with back-off for `assignmentPendingTimeoutMs` (default 3 min). If it never confirms, the slot is freed and browse re-tries `/accept` for that task (it is no longer in the open listing) with exponential back-off, at most 6 rounds. Unknown 5xx, 429 and network errors are treated the same way. |
318
+ | `503 REWRAP_FAILED`, `503 SETTLEMENT_FAILED` whose message says the task was **released** | The backend re-opened the task. (Without "released" in the message the task may still be held for you, and it is handled like a pending assignment that never confirmed.) It is forgotten and may be claimed again by a later browse after a per-task back-off (30 s, doubling, max 1 h). |
319
+ | `409` (`NOT_OPEN`, `OFFER_HELD`, …) | Nothing was claimed; forgotten. |
320
+ | other `4xx` (`SELF_ACCEPT`, `NOT_TARGET_EXECUTOR`, …) | Not re-tried while the task stays listed. |
321
+
322
+ Every one of these emits `task_failed` with the reason; none leaves the task
323
+ in `activeExecutions`.
221
324
 
222
325
  ### Event watching
223
326
 
@@ -240,7 +343,7 @@ const stopAgent = bb.watchAgent(agentId, (agent) => {
240
343
 
241
344
  ```ts
242
345
  const result = await bb.verify({
243
- taskId: 42,
346
+ taskHash, // bytes32 — numeric ids collide across chains
244
347
  taskCategory: 'photography',
245
348
  taskRequirements: 'Photo must show the storefront clearly',
246
349
  evidenceSummary: 'Photo shows 123 Main St storefront',
@@ -290,7 +393,7 @@ const leaderboard = await bb.getLeaderboard(10);
290
393
 
291
394
  ```ts
292
395
  const { rootHash } = await bb.uploadBlob('0x...');
293
- const { data } = await bb.downloadBlob(rootHash);
396
+ const { blob } = await bb.downloadBlob(rootHash); // base64
294
397
  ```
295
398
 
296
399
  ## Low-level API
@@ -1,13 +1,32 @@
1
1
  import { BlindMarket } from '../index.js';
2
- import type { A2ATaskState, AgentCapability, ExecutorProfile, Message } from '../types.js';
2
+ import type { A2APublicTaskMeta, A2ATaskState, AgentCapability, ExecutorProfile, Message } from '../types.js';
3
3
  export interface WorkerRuntimeConfig {
4
4
  apiKey: string;
5
5
  apiBase?: string;
6
6
  displayName: string;
7
7
  capabilities: AgentCapability[];
8
8
  executeTask: ExecuteTaskHandler;
9
+ /**
10
+ * Private key of the wallet that OWNS `apiKey`. The backend registers the
11
+ * API key's owner as the executor and builds `submitEvidence` for that
12
+ * address, so only this key can both decrypt briefs and settle them.
13
+ * start() checks the key against the API key's owner BEFORE registering
14
+ * anything, then registers its uncompressed public key.
15
+ *
16
+ * REQUIRED (this or `existingPrivateKey`): start() throws without a key.
17
+ * Up to 0.5.x a keyless runtime registered a random wallet's public key
18
+ * over the owner's and accepted tasks it could never deliver.
19
+ */
20
+ privateKey?: string;
21
+ /**
22
+ * Restore mode: the owner wallet's key, WITHOUT re-registering on start
23
+ * (the stored profile is kept; see declareSupportedChains). start() throws
24
+ * if this key's address is not the executor the API key resolves to.
25
+ */
9
26
  existingPrivateKey?: string;
27
+ /** Optional cross-check: start() throws if it is not `existingPrivateKey`'s address. */
10
28
  existingAddress?: string;
29
+ /** @deprecated Ignored — the public key is derived from `existingPrivateKey`. */
11
30
  existingPublicKey?: string;
12
31
  minReward?: string;
13
32
  preferredCapabilities?: AgentCapability[];
@@ -15,19 +34,37 @@ export interface WorkerRuntimeConfig {
15
34
  watchIntervalMs?: number;
16
35
  maxConcurrentTasks?: number;
17
36
  /**
18
- * The 0G RPC used to sign + broadcast `submitEvidence` for a 0G task.
19
- * Defaults to the 0G testnet RPC (matches `backend/agents/worker.js`'s
20
- * default). It is 0G ONLY: it never stands in for another chain. For Base
21
- * set `rpcUrls.base`.
37
+ * How long a task may wait for the poster to wrap its brief key (403
38
+ * NEEDS_WRAP) before it is backed off (default 10 min). The wait holds no
39
+ * concurrency slot. Each timeout doubles the back-off: `wrapTimeoutMs`,
40
+ * then 2x, 4x … capped at 24 h.
41
+ */
42
+ wrapTimeoutMs?: number;
43
+ /**
44
+ * How long to keep re-trying /accept while the backend answers 503
45
+ * ASSIGNMENT_PENDING (the assign tx is broadcast, the task is held for this
46
+ * executor). Default 3 min — the backend's own settlement deadline is 120 s.
47
+ * After that the task is re-tried from the browse loop with back-off.
48
+ */
49
+ assignmentPendingTimeoutMs?: number;
50
+ /**
51
+ * The 0G RPC used to sign + broadcast `submitEvidence` for a 0G task. NO
52
+ * DEFAULT (0.5.x defaulted to 0G testnet while `apiBase` defaults to
53
+ * production, so a default runtime accepted mainnet tasks and failed the
54
+ * chainId pin after assignment). It is 0G ONLY: it never stands in for
55
+ * another chain. For Base set `rpcUrls.base`, for Arc `rpcUrls.arc`. Must be the same network the
56
+ * backend at `apiBase` settles on.
22
57
  */
23
58
  rpcUrl?: string;
24
59
  /**
25
60
  * Per-chain RPCs. A task is escrowed on exactly one chain, and
26
- * submitEvidence must be signed on that chain. The runtime DECLARES, as its
61
+ * submitEvidence must be signed on that chain. The runtime declares, as its
27
62
  * `supportedChains`, exactly the chains it has an RPC for — `rpcUrls` keys,
28
- * plus 0G through `rpcUrl` — so the backend only offers it tasks it can
29
- * settle. Without `rpcUrls.base` it is not offered Base tasks (production
30
- * posts new tasks on Base).
63
+ * plus 0G through `rpcUrl`. The backend only STORES that list; it does not
64
+ * filter offers or /accept by it. What keeps the runtime off a chain it
65
+ * cannot settle is client-side: browse skips entries whose `meta.chain` it
66
+ * did not declare, and executeTask fails before running the handler when
67
+ * /accept names such a chain. start() throws when no RPC is configured.
31
68
  */
32
69
  rpcUrls?: Partial<Record<SettlementChain, string>>;
33
70
  }
@@ -35,12 +72,14 @@ export interface WorkerRuntimeConfig {
35
72
  * Chains this runtime's CODE can sign submitEvidence on. What it registers as
36
73
  * its `supportedChains` is the subset it also has an RPC for (declaredChains).
37
74
  */
38
- export declare const SETTLEMENT_CHAINS: readonly ["0g", "base"];
75
+ export declare const SETTLEMENT_CHAINS: readonly ["0g", "base", "arc"];
39
76
  export type SettlementChain = (typeof SETTLEMENT_CHAINS)[number];
40
77
  export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, unknown>>;
41
78
  export interface TaskContext {
42
79
  taskId: string;
43
80
  task: A2ATaskState;
81
+ /** Public metadata from the browse entry (chain, deadline, capabilities). */
82
+ meta?: A2APublicTaskMeta;
44
83
  instructions: string;
45
84
  }
46
85
  export interface TaskExecutionInfo {
@@ -111,18 +150,19 @@ export declare class WorkerRuntime {
111
150
  private running;
112
151
  private paused;
113
152
  private browseTimer?;
114
- private watchTimers;
115
153
  private executions;
154
+ private retries;
155
+ private retryTimers;
116
156
  private listeners;
117
157
  constructor(config: WorkerRuntimeConfig);
118
158
  get isRunning(): boolean;
119
159
  get isPaused(): boolean;
120
160
  get activeExecutions(): TaskExecutionInfo[];
121
161
  /**
122
- * The chains this runtime declares to the backend: those its code can sign
123
- * for AND it has an RPC for. Declaring a chain with no RPC made the backend
124
- * offer tasks the runtime accepted and then could not settle, stranding
125
- * them until the poster's deadline.
162
+ * The chains this runtime declares to the backend and claims tasks on:
163
+ * those its code can sign for AND it has an RPC for. Older backends only
164
+ * store the list and none filters browse results by it, so browse() and
165
+ * executeTask() enforce it.
126
166
  */
127
167
  get declaredChains(): SettlementChain[];
128
168
  /** Get own executor profile (available after start). */
@@ -141,9 +181,11 @@ export declare class WorkerRuntime {
141
181
  * chain this runtime has no RPC for — it would be offered, accept and
142
182
  * strand those tasks. A stored list that is a SUBSET of what the runtime
143
183
  * can settle is left alone: an operator who registered ['base'] through
144
- * the MCP or PATCH meant it. The backend only offers an executor
145
- * tasks on the chains it declared, and a restore never registers otherwise,
146
- * so an executor first registered by an older SDK would keep its old list.
184
+ * the MCP or PATCH meant it. Older backends only store the list (newer ones
185
+ * also filter offers and /accept by it); this runtime's own browse filter
186
+ * uses `declaredChains`, not the stored list — and a restore never
187
+ * registers otherwise, so an executor first registered by an older SDK
188
+ * would keep its old list.
147
189
  *
148
190
  * A /profile response with no `supportedChains` key comes from a backend
149
191
  * that predates the field. That backend would drop the field anyway, and
@@ -167,7 +209,31 @@ export declare class WorkerRuntime {
167
209
  resume(): void;
168
210
  private startBrowseLoop;
169
211
  private browse;
170
- private watchForAssignment;
212
+ /** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
213
+ private inFlight;
214
+ /**
215
+ * Start executing `taskId` if it is not running, not backing off, and a slot
216
+ * is free. `dueAt`: the time a scheduled re-try counts as running at.
217
+ */
218
+ private claim;
219
+ private retryState;
220
+ /**
221
+ * POST /accept, re-trying while the backend says the claim is still ours:
222
+ * 503 ASSIGNMENT_PENDING means the assign tx is broadcast but unconfirmed
223
+ * and the task stays `accepted` for this executor until a retry confirms it
224
+ * (or the backend's sweep releases it). A 503 SETTLEMENT_FAILED seen AFTER a
225
+ * pending answer is the idempotent re-check failing, not a release, so it is
226
+ * re-tried too. Bounded by assignmentPendingTimeoutMs, then AcceptAbandoned
227
+ * with `held: true`. Every other error is thrown as it came.
228
+ */
229
+ private acceptUntilAssigned;
230
+ /**
231
+ * /accept did not hand over the task. Release the slot and decide when (if
232
+ * ever) the task is touched again. Returns the message for `task_failed`.
233
+ */
234
+ private onAcceptFailed;
235
+ /** Re-try one task sooner than the next browse tick (NEEDS_WRAP wait). */
236
+ private scheduleRetry;
171
237
  private executeTask;
172
238
  /**
173
239
  * Decode a wrapped-key hex string — acceptTask()'s wrappedKey field is a