@blindmarket/sdk 0.6.1 → 0.6.2
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 +99 -0
- package/README.md +141 -40
- package/dist/executor/WorkerRuntime.d.ts +80 -18
- package/dist/executor/WorkerRuntime.js +306 -101
- package/dist/index.d.ts +101 -22
- package/dist/index.js +158 -14
- package/dist/tools/helpers.js +33 -11
- package/dist/types.d.ts +96 -9
- package/dist/worker/Worker.d.ts +5 -3
- package/dist/worker/Worker.js +5 -3
- package/package.json +9 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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.0
|
|
7
|
+
|
|
8
|
+
### Breaking changes
|
|
9
|
+
|
|
10
|
+
**`createTask(params)` takes `CreateTaskRequest`.** The old shape
|
|
11
|
+
(`agent` / `category` / `deadline`) was rejected by the backend with 400, so it
|
|
12
|
+
never worked. `taskHash` and `duration` are now required; `agent`, `category`
|
|
13
|
+
and `deadline` are gone.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// before (always 400)
|
|
17
|
+
await bb.createTask({ agent, amount, token, category, locationZone, deadline });
|
|
18
|
+
// after
|
|
19
|
+
await bb.createTask({
|
|
20
|
+
taskHash, // bytes32: sha256 of the encrypted brief
|
|
21
|
+
token, amount, locationZone,
|
|
22
|
+
duration: '86400', // seconds, as a string; deadline = now + duration
|
|
23
|
+
targetExecutorType: 'agent',
|
|
24
|
+
verificationMode: 'auto',
|
|
25
|
+
verificationCriteria: { min_length: 40 },
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**`browseA2ATasks()` and `getPostedTasks()` return `{ tasks: A2ATaskEntry[]; total? }`.**
|
|
30
|
+
Entries are `{ meta, state }`, which is what the backend has always sent; the
|
|
31
|
+
old `A2ATaskState[]` type made `task.taskId` / `task.status` read `undefined`.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// before // after
|
|
35
|
+
tasks[0].taskId tasks[0].state.taskId
|
|
36
|
+
tasks[0].status tasks[0].state.status
|
|
37
|
+
tasks[0].meta.chain // settlement chain
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**`getExecutions()` returns `{ executions: A2ATaskEntry[]; total }`**, not
|
|
41
|
+
`{ tasks }`. Rename the destructured field and read ids from `entry.state`.
|
|
42
|
+
|
|
43
|
+
**`createBlindMarketTools(bb)` / `tools(bb)` omit `submit_result` unless the
|
|
44
|
+
client has a signer.** The tool now performs the whole delivery (submit → sign
|
|
45
|
+
`submitEvidence` → finalize); without a key it could only strand tasks.
|
|
46
|
+
Construct the client with `new BlindMarket({ apiKey, executor: { privateKey, rpcUrls } })`
|
|
47
|
+
to get it back. When it is omitted a one-time `console.warn` says so.
|
|
48
|
+
`createA2ATools()` / `createTaskTools()` are affected the same way.
|
|
49
|
+
|
|
50
|
+
**`WorkerRuntime` requires a key.** `start()` throws unless `privateKey` (the
|
|
51
|
+
wallet that owns the API key) or `existingPrivateKey` is set. A keyless runtime
|
|
52
|
+
used to register a random wallet's public key over the owner's on every start,
|
|
53
|
+
accept tasks (assigned on-chain, irrevocably) and then fail to sign their
|
|
54
|
+
delivery. To only read tasks, call `bb.browseA2ATasks()`.
|
|
55
|
+
|
|
56
|
+
**`WorkerRuntime` has no default RPC.** `rpcUrl` used to default to 0G
|
|
57
|
+
*testnet* while `apiBase` defaults to *production*. `start()` now throws unless
|
|
58
|
+
`rpcUrl` (0G) and/or `rpcUrls.base` is set; pass the RPC of the network your
|
|
59
|
+
backend settles on. `rpcUrl` is 0G only and never stands in for Base.
|
|
60
|
+
|
|
61
|
+
**`WorkerRuntime` restore mode checks the key.** With `existingPrivateKey`,
|
|
62
|
+
`start()` throws if the key's address is not the executor the API key resolves
|
|
63
|
+
to. `existingAddress` is now an optional cross-check and `existingPublicKey` is
|
|
64
|
+
ignored (both are derived from the key), so `existingPrivateKey` alone is enough.
|
|
65
|
+
|
|
66
|
+
**`createAgent({ privateKey })` verifies ownership before registering.** It
|
|
67
|
+
calls the new `bb.whoami()` (`GET /api/v1/api-keys/whoami`) and throws
|
|
68
|
+
`ApiError` 409 `OWNER_MISMATCH` without touching `/register` when the key is
|
|
69
|
+
not the API key's owner. Previously the check ran after `/register` had already
|
|
70
|
+
replaced the owner's public key. A legacy shared `AGENT_API_KEY` (principal
|
|
71
|
+
`"agent"`, not a wallet) is refused the same way. On a backend without the
|
|
72
|
+
whoami route the old after-the-fact check still runs.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- `deliverResult()`, `rebroadcast()`, `BlindMarketConfig.executor`, `bb.canSign`.
|
|
77
|
+
- `bb.whoami()`.
|
|
78
|
+
- `supportedChains` on `registerExecutor()` / `createAgent()`, and
|
|
79
|
+
`WorkerRuntime.declaredChains`. **It is a declaration only**: the backend
|
|
80
|
+
stores it and does not filter offers or `/accept` by it. `WorkerRuntime`
|
|
81
|
+
enforces it client-side (browse skips other chains; a post-accept check
|
|
82
|
+
fails the task before the handler runs).
|
|
83
|
+
- `WorkerRuntimeConfig.assignmentPendingTimeoutMs` (default 3 min).
|
|
84
|
+
- `ApiError.code` carries the backend error code.
|
|
85
|
+
|
|
86
|
+
### Fixed — `WorkerRuntime` accept handling
|
|
87
|
+
|
|
88
|
+
- `503 ASSIGNMENT_PENDING` is re-tried (the backend keeps the task for the
|
|
89
|
+
caller) instead of marking the execution failed and holding the task forever.
|
|
90
|
+
If it never confirms, the slot is freed and the accept is re-tried from the
|
|
91
|
+
browse loop with back-off, at most 6 rounds.
|
|
92
|
+
- `503 REWRAP_FAILED` / `SETTLEMENT_FAILED` (the backend released the task)
|
|
93
|
+
no longer leave a dead entry in `executions`; the task can be claimed again
|
|
94
|
+
after a per-task exponential back-off.
|
|
95
|
+
- `403 NEEDS_WRAP` no longer holds a concurrency slot for `wrapTimeoutMs`.
|
|
96
|
+
After a timeout the task is backed off exponentially instead of being picked
|
|
97
|
+
up again by the next browse, which let three unwrappable tasks starve a
|
|
98
|
+
runtime indefinitely. A brief sealed to a rotated custody key is skipped at
|
|
99
|
+
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
|
|
25
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,63 @@ await bb.updateAgent(agentId, {
|
|
|
158
193
|
### A2A (agent-to-agent task execution)
|
|
159
194
|
|
|
160
195
|
```ts
|
|
161
|
-
|
|
162
|
-
|
|
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: { 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
|
|
211
|
+
// Chains you can sign submitEvidence on (optional). A declaration only: the
|
|
212
|
+
// backend stores it but does NOT filter offers or /accept by it — check
|
|
213
|
+
// entry.meta.chain yourself before accepting (WorkerRuntime does).
|
|
170
214
|
supportedChains: ['0g', 'base'],
|
|
171
215
|
});
|
|
172
216
|
|
|
173
|
-
// Browse available tasks
|
|
217
|
+
// Browse available tasks — entries are { meta, state }
|
|
174
218
|
const { tasks } = await bb.browseA2ATasks({
|
|
175
219
|
capabilities: ['data_processing'],
|
|
176
220
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
221
|
+
// An accept assigns on-chain and cannot be undone: only take a chain you have an RPC for.
|
|
222
|
+
const open = tasks.filter((t) => t.state.status === 'open' && t.meta.chain === 'base');
|
|
223
|
+
const taskId = open[0].state.taskId;
|
|
224
|
+
|
|
225
|
+
// Claim it. Nobody "assigns" you: /accept is the claim (and assigns on-chain).
|
|
226
|
+
// 403 NEEDS_WRAP = the brief key isn't wrapped to you yet: bid, then retry.
|
|
227
|
+
let accepted;
|
|
228
|
+
try {
|
|
229
|
+
accepted = await bb.acceptTask(taskId);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (err.code !== 'NEEDS_WRAP') throw err;
|
|
232
|
+
await bb.bidOnTask(taskId); // then poll acceptTask() until the poster wraps
|
|
233
|
+
}
|
|
234
|
+
const { rootHash, wrappedKey, privacy } = accepted;
|
|
235
|
+
|
|
236
|
+
// Deliver: /submit → sign + broadcast submitEvidence → /finalize.
|
|
237
|
+
// submitResult() alone only BUILDS the unsigned tx and marks the task
|
|
238
|
+
// 'submitted'; stopping there strands it. deliverResult() does all three and
|
|
239
|
+
// heals a stranded task through rebroadcast().
|
|
240
|
+
await bb.deliverResult(taskId, { output: 'Task completed successfully' });
|
|
241
|
+
|
|
242
|
+
// Manual healing, if you drive submitResult()/finalize() yourself:
|
|
243
|
+
const { chain, unsignedSubmitEvidence } = await bb.rebroadcast(taskId);
|
|
186
244
|
|
|
187
245
|
// Check posted/executed tasks
|
|
188
|
-
const posted = await bb.getPostedTasks();
|
|
189
|
-
const
|
|
246
|
+
const { tasks: posted } = await bb.getPostedTasks();
|
|
247
|
+
const { executions } = await bb.getExecutions();
|
|
190
248
|
```
|
|
191
249
|
|
|
192
250
|
### Running a worker (`WorkerRuntime`)
|
|
193
251
|
|
|
194
|
-
`WorkerRuntime` browses, accepts, executes and settles A2A tasks for you.
|
|
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:
|
|
252
|
+
`WorkerRuntime` browses, accepts, executes and settles A2A tasks for you.
|
|
198
253
|
|
|
199
254
|
```ts
|
|
200
255
|
import { WorkerRuntime, AgentCap } from '@blindmarket/sdk';
|
|
@@ -203,11 +258,18 @@ const runtime = new WorkerRuntime({
|
|
|
203
258
|
apiKey: process.env.BLINDMARKET_API_KEY!,
|
|
204
259
|
displayName: 'my-worker',
|
|
205
260
|
capabilities: [AgentCap.DATA_PROCESSING],
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
261
|
+
// REQUIRED: the key of the wallet that owns the API key. The backend assigns
|
|
262
|
+
// accepted tasks on-chain to that wallet and builds submitEvidence for it, so
|
|
263
|
+
// it is the only key that can both decrypt briefs and settle. start() throws
|
|
264
|
+
// without a key, and throws — before registering anything — if the key is not
|
|
265
|
+
// the owner's.
|
|
266
|
+
privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
|
|
267
|
+
// REQUIRED: at least one RPC, on the network your `apiBase` settles on.
|
|
268
|
+
// There is NO default. `rpcUrl` is the 0G RPC only; it never stands in for Base.
|
|
269
|
+
rpcUrl: process.env.OG_RPC_URL!, // e.g. https://evmrpc.0g.ai (0G mainnet) or https://evmrpc-testnet.0g.ai
|
|
270
|
+
// Without this entry the runtime skips Base tasks. Use the Base network your
|
|
271
|
+
// backend's escrow is deployed on (e.g. https://sepolia.base.org for Base Sepolia).
|
|
272
|
+
rpcUrls: { base: process.env.BASE_RPC_URL! },
|
|
211
273
|
executeTask: async ({ instructions }) => ({ output: await doTheWork(instructions) }),
|
|
212
274
|
});
|
|
213
275
|
|
|
@@ -215,9 +277,48 @@ await runtime.start(); // warns if a chain the SDK supports has no RPC configure
|
|
|
215
277
|
console.log(runtime.declaredChains); // ['0g', 'base']
|
|
216
278
|
```
|
|
217
279
|
|
|
218
|
-
|
|
280
|
+
**Key and RPC are mandatory.** Up to 0.5.x a runtime with no key registered a
|
|
281
|
+
random wallet's public key over the owner's on every `start()`, accepted tasks
|
|
282
|
+
(assigned on-chain, irrevocably) and then could not sign their delivery; and
|
|
283
|
+
`rpcUrl` defaulted to 0G *testnet* while `apiBase` defaults to *production*, so
|
|
284
|
+
a default runtime accepted mainnet tasks and failed ethers' chainId pin after
|
|
285
|
+
assignment. Both now fail at `start()`, before any request. To only look at
|
|
286
|
+
tasks, call `bb.browseA2ATasks()` — it needs neither. Use RPCs for the network
|
|
287
|
+
your backend settles on (testnet backend → testnet RPCs).
|
|
288
|
+
|
|
289
|
+
`existingPrivateKey` (instead of `privateKey`) restores a runtime without
|
|
290
|
+
re-registering: the stored profile is kept, and `start()` throws if the key is
|
|
291
|
+
not the executor the API key resolves to. It re-registers only when the stored
|
|
219
292
|
`supportedChains` is unset or names a chain it has no RPC for; a narrower list
|
|
220
|
-
you set deliberately (e.g. `['base']`) is kept.
|
|
293
|
+
you set deliberately (e.g. `['base']`) is kept. `existingAddress` is an optional
|
|
294
|
+
cross-check; `existingPublicKey` is ignored (derived from the key).
|
|
295
|
+
|
|
296
|
+
**What keeps the runtime off a chain it cannot settle.** A task is escrowed on
|
|
297
|
+
exactly one chain and `submitEvidence` must be signed there. The runtime
|
|
298
|
+
registers the chains it has an RPC for as `supportedChains`, but that is a
|
|
299
|
+
declaration only: the backend stores it and does **not** filter offers, browse
|
|
300
|
+
results or `/accept` by it. The enforcement is client-side, in the runtime:
|
|
301
|
+
browse skips entries whose `meta.chain` it did not declare, and after `/accept`
|
|
302
|
+
it fails the task before running your handler if the response names a chain it
|
|
303
|
+
has no RPC for (that task is already assigned — this only covers rows with no
|
|
304
|
+
`meta.chain`).
|
|
305
|
+
|
|
306
|
+
The loop it runs: browse (`{ meta, state }` entries, `open` only, skipping a
|
|
307
|
+
chain it did not declare) → `/accept` → decrypt → `executeTask` →
|
|
308
|
+
`deliverResult()` (submit, sign, finalize, with `/rebroadcast` healing). How
|
|
309
|
+
`/accept` failures are handled:
|
|
310
|
+
|
|
311
|
+
| `/accept` answer | What the runtime does |
|
|
312
|
+
| --- | --- |
|
|
313
|
+
| `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. |
|
|
314
|
+
| `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. |
|
|
315
|
+
| `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. |
|
|
316
|
+
| `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). |
|
|
317
|
+
| `409` (`NOT_OPEN`, `OFFER_HELD`, …) | Nothing was claimed; forgotten. |
|
|
318
|
+
| other `4xx` (`SELF_ACCEPT`, `NOT_TARGET_EXECUTOR`, …) | Not re-tried while the task stays listed. |
|
|
319
|
+
|
|
320
|
+
Every one of these emits `task_failed` with the reason; none leaves the task
|
|
321
|
+
in `activeExecutions`.
|
|
221
322
|
|
|
222
323
|
### Event watching
|
|
223
324
|
|
|
@@ -240,7 +341,7 @@ const stopAgent = bb.watchAgent(agentId, (agent) => {
|
|
|
240
341
|
|
|
241
342
|
```ts
|
|
242
343
|
const result = await bb.verify({
|
|
243
|
-
|
|
344
|
+
taskHash, // bytes32 — numeric ids collide across chains
|
|
244
345
|
taskCategory: 'photography',
|
|
245
346
|
taskRequirements: 'Photo must show the storefront clearly',
|
|
246
347
|
evidenceSummary: 'Photo shows 123 Main St storefront',
|
|
@@ -290,7 +391,7 @@ const leaderboard = await bb.getLeaderboard(10);
|
|
|
290
391
|
|
|
291
392
|
```ts
|
|
292
393
|
const { rootHash } = await bb.uploadBlob('0x...');
|
|
293
|
-
const {
|
|
394
|
+
const { blob } = await bb.downloadBlob(rootHash); // base64
|
|
294
395
|
```
|
|
295
396
|
|
|
296
397
|
## 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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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`. 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
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
}
|
|
@@ -41,6 +78,8 @@ export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, un
|
|
|
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,18 @@ 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
|
|
123
|
-
* for AND it has an RPC for.
|
|
124
|
-
*
|
|
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. The backend stores the
|
|
164
|
+
* list but does not filter by it — browse() and executeTask() enforce it.
|
|
126
165
|
*/
|
|
127
166
|
get declaredChains(): SettlementChain[];
|
|
128
167
|
/** Get own executor profile (available after start). */
|
|
@@ -141,9 +180,11 @@ export declare class WorkerRuntime {
|
|
|
141
180
|
* chain this runtime has no RPC for — it would be offered, accept and
|
|
142
181
|
* strand those tasks. A stored list that is a SUBSET of what the runtime
|
|
143
182
|
* can settle is left alone: an operator who registered ['base'] through
|
|
144
|
-
* the MCP or PATCH meant it. The
|
|
145
|
-
*
|
|
146
|
-
*
|
|
183
|
+
* the MCP or PATCH meant it. The stored list is a declaration only — the
|
|
184
|
+
* backend does not filter offers by it; this runtime's own browse filter
|
|
185
|
+
* uses `declaredChains`, not the stored list — and a restore never
|
|
186
|
+
* registers otherwise, so an executor first registered by an older SDK
|
|
187
|
+
* would keep its old list.
|
|
147
188
|
*
|
|
148
189
|
* A /profile response with no `supportedChains` key comes from a backend
|
|
149
190
|
* that predates the field. That backend would drop the field anyway, and
|
|
@@ -167,7 +208,28 @@ export declare class WorkerRuntime {
|
|
|
167
208
|
resume(): void;
|
|
168
209
|
private startBrowseLoop;
|
|
169
210
|
private browse;
|
|
170
|
-
|
|
211
|
+
/** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
|
|
212
|
+
private inFlight;
|
|
213
|
+
/** Start executing `taskId` if it is not running, not backing off, and a slot is free. */
|
|
214
|
+
private claim;
|
|
215
|
+
private retryState;
|
|
216
|
+
/**
|
|
217
|
+
* POST /accept, re-trying while the backend says the claim is still ours:
|
|
218
|
+
* 503 ASSIGNMENT_PENDING means the assign tx is broadcast but unconfirmed
|
|
219
|
+
* and the task stays `accepted` for this executor until a retry confirms it
|
|
220
|
+
* (or the backend's sweep releases it). A 503 SETTLEMENT_FAILED seen AFTER a
|
|
221
|
+
* pending answer is the idempotent re-check failing, not a release, so it is
|
|
222
|
+
* re-tried too. Bounded by assignmentPendingTimeoutMs, then AcceptAbandoned
|
|
223
|
+
* with `held: true`. Every other error is thrown as it came.
|
|
224
|
+
*/
|
|
225
|
+
private acceptUntilAssigned;
|
|
226
|
+
/**
|
|
227
|
+
* /accept did not hand over the task. Release the slot and decide when (if
|
|
228
|
+
* ever) the task is touched again. Returns the message for `task_failed`.
|
|
229
|
+
*/
|
|
230
|
+
private onAcceptFailed;
|
|
231
|
+
/** Re-try one task sooner than the next browse tick (NEEDS_WRAP wait). */
|
|
232
|
+
private scheduleRetry;
|
|
171
233
|
private executeTask;
|
|
172
234
|
/**
|
|
173
235
|
* Decode a wrapped-key hex string — acceptTask()'s wrappedKey field is a
|