@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 +132 -0
- package/README.md +145 -42
- package/dist/executor/WorkerRuntime.d.ts +85 -19
- package/dist/executor/WorkerRuntime.js +319 -104
- 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 +101 -9
- package/dist/worker/Worker.d.ts +5 -3
- package/dist/worker/Worker.js +5 -3
- package/package.json +10 -5
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
|
|
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,64 @@ 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: { 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
|
|
170
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
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); // ['
|
|
278
|
+
console.log(runtime.declaredChains); // ['base', 'arc']
|
|
216
279
|
```
|
|
217
280
|
|
|
218
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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`, 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
|
|
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
|
}
|
|
@@ -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
|
|
123
|
-
* for AND it has an RPC for.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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.
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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
|
-
|
|
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
|