@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/dist/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { ethers } from 'ethers';
2
- import type { Address, Hex, RootHash, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput, A2ATaskState, CreateAgentParams, CreateAgentResult } from './types.js';
2
+ import type { Address, Hex, RootHash, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput, A2ATaskEntry, CreateAgentParams, CreateAgentResult, CreateTaskRequest } from './types.js';
3
3
  export interface BlindMarketConfig {
4
4
  /** Backend API base URL (default: https://api.blindmarket.xyz) */
5
5
  apiBase?: string;
6
6
  /** API key — shared AGENT_API_KEY or device-flow token */
7
7
  apiKey: string;
8
+ /**
9
+ * Executor signer: the private key of the wallet that owns `apiKey`, plus
10
+ * the RPC(s) to broadcast `submitEvidence` on. Default for `createAgent()`
11
+ * and `deliverResult()`, and what enables the `submit_result` tool — tools
12
+ * never take a key as an argument. Stays in-process; never sent to the backend.
13
+ */
14
+ executor?: DeliverSigner;
8
15
  }
9
16
  export interface DeployAgentParams {
10
17
  name: string;
@@ -28,7 +35,18 @@ export interface DeployedAgent {
28
35
  declare class ApiError extends Error {
29
36
  status: number;
30
37
  body?: unknown | undefined;
31
- constructor(status: number, message: string, body?: unknown | undefined);
38
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
39
+ code?: string | undefined;
40
+ constructor(status: number, message: string, body?: unknown | undefined,
41
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
42
+ code?: string | undefined);
43
+ }
44
+ /** Per-chain RPC URLs for signing `submitEvidence` — a task is escrowed on exactly one chain. */
45
+ export interface DeliverSigner {
46
+ /** Private key of the executor wallet (the API key's owner) — `submitEvidence` is `onlyWorker`. */
47
+ privateKey: string;
48
+ /** RPC per chain. No default: a missing entry refuses the task's chain rather than guessing a network. */
49
+ rpcUrls: Partial<Record<string, string | undefined>>;
32
50
  }
33
51
  /**
34
52
  * BlindMarket REST API client.
@@ -53,7 +71,10 @@ declare class ApiError extends Error {
53
71
  export declare class BlindMarket {
54
72
  private apiBase;
55
73
  private apiKey;
74
+ private executor?;
56
75
  constructor(config: BlindMarketConfig);
76
+ /** True when an executor signer was configured (see BlindMarketConfig.executor). */
77
+ get canSign(): boolean;
57
78
  /**
58
79
  * Tool definitions for AI agent frameworks. Access framework-specific formats
59
80
  * via property — no need to remember adapter function names.
@@ -91,16 +112,11 @@ export declare class BlindMarket {
91
112
  getTask(id: string): Promise<TaskDetail>;
92
113
  /**
93
114
  * Build an unsigned `createTask` transaction.
94
- * You must sign and broadcast it with your wallet.
115
+ * You must sign and broadcast it with your wallet (the API key's owner is
116
+ * the poster), then index it via `POST /api/v1/a2a/tasks/index` for A2A.
117
+ * The deadline is set on-chain as now + `duration` seconds.
95
118
  */
96
- createTask(params: {
97
- agent: Address;
98
- amount: string;
99
- token: Address;
100
- category: string;
101
- locationZone: string;
102
- deadline: number;
103
- }): Promise<CreateTaskTx>;
119
+ createTask(params: CreateTaskRequest): Promise<CreateTaskTx>;
104
120
  /**
105
121
  * Build an unsigned `assignWorker` transaction.
106
122
  */
@@ -146,22 +162,48 @@ export declare class BlindMarket {
146
162
  */
147
163
  deployAgent(params: DeployAgentParams): Promise<DeployedAgent>;
148
164
  /**
149
- * One-shot agent creation: generates a secp256k1 wallet, then registers the
150
- * agent as an executor in the A2A marketplace with the generated wallet
151
- * address and public key. Replaces the manual two-step flow of generating a
152
- * wallet, then calling registerExecutor().
165
+ * One-shot executor registration in the A2A marketplace.
153
166
  *
154
- * The private key is returned **once** in the response — store it securely.
167
+ * The registered executor is ALWAYS the wallet that owns the API key — the
168
+ * backend takes the address from auth, never from the request. Briefs are
169
+ * wrapped to the public key registered here, and `submitEvidence` is built
170
+ * for the owner address. So pass `privateKey` (or set
171
+ * `BlindMarketConfig.executor`) — the OWNER wallet's key: its uncompressed
172
+ * public key is registered, the key never leaves this process, and this
173
+ * throws 409 OWNER_MISMATCH — BEFORE registering anything — when the key is
174
+ * not the API key's owner (checked through `whoami()`).
175
+ *
176
+ * Without a key a random secp256k1 wallet is generated and its private key
177
+ * returned **once** (store it securely). That wallet can decrypt briefs, but
178
+ * it is not the registered executor address, so it cannot sign
179
+ * `submitEvidence` for tasks the owner accepts — use it only when delivery
180
+ * goes through another signer (e.g. the backend relay).
155
181
  *
156
182
  * @example
157
183
  * const { executor, wallet } = await bb.createAgent({
184
+ * privateKey: process.env.EXECUTOR_PRIVATE_KEY!, // the API key owner's wallet (or set BlindMarketConfig.executor)
158
185
  * displayName: 'DataBot',
159
186
  * capabilities: [AgentCap.DATA_PROCESSING, AgentCap.WEB_RESEARCH],
160
187
  * minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
161
188
  * });
162
- * console.log(`Agent ${wallet.address} registered as ${executor.address}`);
189
+ * console.log(`Registered executor ${executor.address}`);
163
190
  */
164
191
  createAgent(params: CreateAgentParams): Promise<CreateAgentResult>;
192
+ /**
193
+ * The wallet this API key authenticates as (`GET /api/v1/api-keys/whoami`).
194
+ * /a2a/register and /accept act for `address`. A legacy shared
195
+ * AGENT_API_KEY resolves to the non-wallet principal `"agent"`.
196
+ */
197
+ whoami(): Promise<{
198
+ address: string;
199
+ addresses?: string[];
200
+ }>;
201
+ /**
202
+ * Throws 409 OWNER_MISMATCH, without side effects, when `address` is not
203
+ * the API key's owner. Returns false only when the backend has no whoami
204
+ * route (404 / a non-JSON 404 page) and nothing could be checked.
205
+ */
206
+ private assertOwnerKey;
165
207
  /** List deployed agents, optionally filtered by owner address. */
166
208
  listAgents(ownerAddress?: string): Promise<DeployedAgentInfo[]>;
167
209
  /** Get a single deployed agent by ID. */
@@ -187,7 +229,12 @@ export declare class BlindMarket {
187
229
  tools: object[];
188
230
  minReward: string;
189
231
  }>): Promise<DeployedAgentInfo>;
190
- /** Register as an A2A agent executor (worker-side). */
232
+ /**
233
+ * Register as an A2A agent executor (worker-side). The executor address is
234
+ * the API key's owner wallet (any `address` sent is ignored). `publicKey`
235
+ * must be uncompressed secp256k1 hex, 130 chars, leading `04`, no 0x —
236
+ * `new ethers.Wallet(pk).signingKey.publicKey.slice(2)`, NOT `wallet.publicKey`.
237
+ */
191
238
  registerExecutor(params: RegisterExecutorInput): Promise<{
192
239
  agent: ExecutorProfile;
193
240
  }>;
@@ -199,12 +246,17 @@ export declare class BlindMarket {
199
246
  getExecutorProfile(): Promise<{
200
247
  agent: ExecutorProfile;
201
248
  }>;
202
- /** Browse A2A tasks available for execution. */
249
+ /**
250
+ * Browse A2A tasks available for execution. Each entry is `{ meta, state }`
251
+ * — the id and status live on `state` (`entry.state.taskId`), the chain and
252
+ * deadline on `meta`.
253
+ */
203
254
  browseA2ATasks(params?: {
204
255
  capabilities?: string[];
205
256
  minReputation?: number;
206
257
  }): Promise<{
207
- tasks: A2ATaskState[];
258
+ tasks: A2ATaskEntry[];
259
+ total?: number;
208
260
  }>;
209
261
  /** Register intent to accept a task (bid). */
210
262
  bidOnTask(taskId: string): Promise<void>;
@@ -266,13 +318,40 @@ export declare class BlindMarket {
266
318
  };
267
319
  reconciled?: boolean;
268
320
  }>;
321
+ /**
322
+ * Rebuild the unsigned `submitEvidence` for a task stranded in 'submitted'
323
+ * — `/submit` flips the state when the tx is BUILT, so a crash before the
324
+ * broadcast leaves `finalize()` failing with NOT_SUBMITTED_ON_CHAIN and
325
+ * `submitResult()` refusing with INVALID_STATE. Sign + broadcast the
326
+ * returned tx on `chain`, then call `finalize()`. 409 ALREADY_SUBMITTED
327
+ * means the evidence did land — just call `finalize()`.
328
+ */
329
+ rebroadcast(taskId: string): Promise<{
330
+ taskId: string;
331
+ onChainTaskId?: string;
332
+ /** A string, not a union — see submitResult(). */
333
+ chain?: string;
334
+ evidenceHash?: Hex;
335
+ unsignedSubmitEvidence?: Record<string, unknown> | null;
336
+ }>;
337
+ /**
338
+ * Deliver a result end to end: `submitResult()` → sign + broadcast the
339
+ * unsigned `submitEvidence` on the chain the backend names → `finalize()`.
340
+ * Safe to re-call on a task stranded in 'submitted': INVALID_STATE at submit
341
+ * and NOT_SUBMITTED_ON_CHAIN at finalize both heal through `rebroadcast()`.
342
+ */
343
+ deliverResult(taskId: string, resultData: Record<string, unknown>, signerOverride?: DeliverSigner): Promise<Awaited<ReturnType<BlindMarket['finalize']>> & {
344
+ submitTxHash?: string;
345
+ }>;
269
346
  /** Get tasks posted by the authenticated user. */
270
347
  getPostedTasks(): Promise<{
271
- tasks: A2ATaskState[];
348
+ tasks: A2ATaskEntry[];
349
+ total?: number;
272
350
  }>;
273
351
  /** Get tasks executed by the authenticated user. */
274
352
  getExecutions(address?: string): Promise<{
275
- tasks: A2ATaskState[];
353
+ executions: A2ATaskEntry[];
354
+ total: number;
276
355
  }>;
277
356
  /**
278
357
  * Trigger TEE / AI verification for a task.
package/dist/index.js CHANGED
@@ -3,10 +3,14 @@ import { ethers } from 'ethers';
3
3
  class ApiError extends Error {
4
4
  status;
5
5
  body;
6
- constructor(status, message, body) {
6
+ code;
7
+ constructor(status, message, body,
8
+ /** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
9
+ code) {
7
10
  super(message);
8
11
  this.status = status;
9
12
  this.body = body;
13
+ this.code = code;
10
14
  this.name = 'ApiError';
11
15
  }
12
16
  }
@@ -34,9 +38,15 @@ class ApiError extends Error {
34
38
  export class BlindMarket {
35
39
  apiBase;
36
40
  apiKey;
41
+ executor;
37
42
  constructor(config) {
38
43
  this.apiBase = config.apiBase ?? 'https://api.blindmarket.xyz';
39
44
  this.apiKey = config.apiKey;
45
+ this.executor = config.executor;
46
+ }
47
+ /** True when an executor signer was configured (see BlindMarketConfig.executor). */
48
+ get canSign() {
49
+ return !!this.executor;
40
50
  }
41
51
  // ── Tools ─────────────────────────────────────────────────────────────────
42
52
  /**
@@ -76,7 +86,7 @@ export class BlindMarket {
76
86
  });
77
87
  const json = await res.json();
78
88
  if (!json.success) {
79
- throw new ApiError(res.status, json.error?.message ?? `HTTP ${res.status}`, json);
89
+ throw new ApiError(res.status, json.error?.message ?? `HTTP ${res.status}`, json, json.error?.code);
80
90
  }
81
91
  return json.data;
82
92
  }
@@ -101,7 +111,9 @@ export class BlindMarket {
101
111
  }
102
112
  /**
103
113
  * Build an unsigned `createTask` transaction.
104
- * You must sign and broadcast it with your wallet.
114
+ * You must sign and broadcast it with your wallet (the API key's owner is
115
+ * the poster), then index it via `POST /api/v1/a2a/tasks/index` for A2A.
116
+ * The deadline is set on-chain as now + `duration` seconds.
105
117
  */
106
118
  async createTask(params) {
107
119
  return this.req('POST', '/api/v1/tasks', params);
@@ -151,28 +163,42 @@ export class BlindMarket {
151
163
  return this.req('POST', '/api/v1/agents/deploy', params);
152
164
  }
153
165
  /**
154
- * One-shot agent creation: generates a secp256k1 wallet, then registers the
155
- * agent as an executor in the A2A marketplace with the generated wallet
156
- * address and public key. Replaces the manual two-step flow of generating a
157
- * wallet, then calling registerExecutor().
166
+ * One-shot executor registration in the A2A marketplace.
167
+ *
168
+ * The registered executor is ALWAYS the wallet that owns the API key — the
169
+ * backend takes the address from auth, never from the request. Briefs are
170
+ * wrapped to the public key registered here, and `submitEvidence` is built
171
+ * for the owner address. So pass `privateKey` (or set
172
+ * `BlindMarketConfig.executor`) — the OWNER wallet's key: its uncompressed
173
+ * public key is registered, the key never leaves this process, and this
174
+ * throws 409 OWNER_MISMATCH — BEFORE registering anything — when the key is
175
+ * not the API key's owner (checked through `whoami()`).
158
176
  *
159
- * The private key is returned **once** in the response — store it securely.
177
+ * Without a key a random secp256k1 wallet is generated and its private key
178
+ * returned **once** (store it securely). That wallet can decrypt briefs, but
179
+ * it is not the registered executor address, so it cannot sign
180
+ * `submitEvidence` for tasks the owner accepts — use it only when delivery
181
+ * goes through another signer (e.g. the backend relay).
160
182
  *
161
183
  * @example
162
184
  * const { executor, wallet } = await bb.createAgent({
185
+ * privateKey: process.env.EXECUTOR_PRIVATE_KEY!, // the API key owner's wallet (or set BlindMarketConfig.executor)
163
186
  * displayName: 'DataBot',
164
187
  * capabilities: [AgentCap.DATA_PROCESSING, AgentCap.WEB_RESEARCH],
165
188
  * minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
166
189
  * });
167
- * console.log(`Agent ${wallet.address} registered as ${executor.address}`);
190
+ * console.log(`Registered executor ${executor.address}`);
168
191
  */
169
192
  async createAgent(params) {
170
- const wallet = ethers.Wallet.createRandom();
193
+ // With a key (params.privateKey, or BlindMarketConfig.executor) the agent
194
+ // IS the API key owner's wallet, the only one that can settle. Without one
195
+ // a random wallet is generated, as before — see the JSDoc for its limits.
196
+ const privateKey = params.privateKey ?? this.executor?.privateKey;
197
+ const wallet = privateKey ? new ethers.Wallet(privateKey) : ethers.Wallet.createRandom();
171
198
  // The uncompressed key (0x04…): /register requires it, and posters wrap
172
199
  // brief keys to it. `wallet.publicKey` is the compressed form in ethers v6.
173
200
  const publicKey = wallet.signingKey.publicKey;
174
201
  const executor = {
175
- address: wallet.address,
176
202
  displayName: params.displayName,
177
203
  capabilities: params.capabilities,
178
204
  publicKey: publicKey.slice(2), // strip 0x prefix — backend expects raw hex
@@ -182,7 +208,17 @@ export class BlindMarket {
182
208
  preferredCapabilities: params.preferredCapabilities,
183
209
  supportedChains: params.supportedChains,
184
210
  };
211
+ // Only when the caller supplied the key: they are claiming to be the owner.
212
+ // Checked BEFORE /register, which upserts `publicKey` over the owner's
213
+ // record — a mismatch found afterwards has already redirected every new
214
+ // brief to a key whose wallet cannot sign submitEvidence.
215
+ const ownerChecked = privateKey ? await this.assertOwnerKey(wallet.address) : false;
185
216
  const result = await this.registerExecutor(executor);
217
+ // Backends without /api-keys/whoami could not be checked up front.
218
+ if (privateKey && !ownerChecked && result.agent.address.toLowerCase() !== wallet.address.toLowerCase()) {
219
+ throw new ApiError(409, `Registered executor is ${result.agent.address} (the API key's owner) but privateKey belongs to ${wallet.address}. ` +
220
+ 'This backend has no /api-keys/whoami, so the mismatch could only be seen after registering: briefs are now wrapped to a key whose wallet cannot sign submitEvidence — re-run with the owner wallet\'s key, or mint an API key signed in as this wallet.', undefined, 'OWNER_MISMATCH');
221
+ }
186
222
  return {
187
223
  executor: result.agent,
188
224
  wallet: {
@@ -192,6 +228,35 @@ export class BlindMarket {
192
228
  },
193
229
  };
194
230
  }
231
+ /**
232
+ * The wallet this API key authenticates as (`GET /api/v1/api-keys/whoami`).
233
+ * /a2a/register and /accept act for `address`. A legacy shared
234
+ * AGENT_API_KEY resolves to the non-wallet principal `"agent"`.
235
+ */
236
+ async whoami() {
237
+ return this.req('GET', '/api/v1/api-keys/whoami');
238
+ }
239
+ /**
240
+ * Throws 409 OWNER_MISMATCH, without side effects, when `address` is not
241
+ * the API key's owner. Returns false only when the backend has no whoami
242
+ * route (404 / a non-JSON 404 page) and nothing could be checked.
243
+ */
244
+ async assertOwnerKey(address) {
245
+ let owner;
246
+ try {
247
+ owner = (await this.whoami()).address;
248
+ }
249
+ catch (err) {
250
+ if (err instanceof SyntaxError || (err instanceof ApiError && err.status === 404))
251
+ return false;
252
+ throw err;
253
+ }
254
+ if (typeof owner !== 'string' || owner.toLowerCase() !== address.toLowerCase()) {
255
+ throw new ApiError(409, `This API key belongs to ${owner} but privateKey belongs to ${address}. Nothing was registered. ` +
256
+ "The executor is always the API key's owner, and only that wallet can sign submitEvidence — use the owner wallet's key, or mint an API key signed in as this wallet.", undefined, 'OWNER_MISMATCH');
257
+ }
258
+ return true;
259
+ }
195
260
  /** List deployed agents, optionally filtered by owner address. */
196
261
  async listAgents(ownerAddress) {
197
262
  const qs = ownerAddress ? `?owner=${ownerAddress}` : '';
@@ -229,7 +294,12 @@ export class BlindMarket {
229
294
  return this.req('PATCH', `/api/v1/agents/${id}`, patch);
230
295
  }
231
296
  // ── A2A executor registration ───────────────────────────────────────────
232
- /** Register as an A2A agent executor (worker-side). */
297
+ /**
298
+ * Register as an A2A agent executor (worker-side). The executor address is
299
+ * the API key's owner wallet (any `address` sent is ignored). `publicKey`
300
+ * must be uncompressed secp256k1 hex, 130 chars, leading `04`, no 0x —
301
+ * `new ethers.Wallet(pk).signingKey.publicKey.slice(2)`, NOT `wallet.publicKey`.
302
+ */
233
303
  async registerExecutor(params) {
234
304
  return this.req('POST', '/api/v1/a2a/register', params);
235
305
  }
@@ -243,10 +313,14 @@ export class BlindMarket {
243
313
  return this.req('GET', '/api/v1/a2a/profile');
244
314
  }
245
315
  // ── A2A task lifecycle ───────────────────────────────────────────────────
246
- /** Browse A2A tasks available for execution. */
316
+ /**
317
+ * Browse A2A tasks available for execution. Each entry is `{ meta, state }`
318
+ * — the id and status live on `state` (`entry.state.taskId`), the chain and
319
+ * deadline on `meta`.
320
+ */
247
321
  async browseA2ATasks(params) {
248
322
  const qs = new URLSearchParams();
249
- if (params?.capabilities)
323
+ if (params?.capabilities?.length)
250
324
  qs.set('capabilities', params.capabilities.join(','));
251
325
  if (params?.minReputation != null)
252
326
  qs.set('minReputation', String(params.minReputation));
@@ -287,6 +361,76 @@ export class BlindMarket {
287
361
  async finalize(taskId) {
288
362
  return this.req('POST', `/api/v1/a2a/tasks/${taskId}/finalize`);
289
363
  }
364
+ /**
365
+ * Rebuild the unsigned `submitEvidence` for a task stranded in 'submitted'
366
+ * — `/submit` flips the state when the tx is BUILT, so a crash before the
367
+ * broadcast leaves `finalize()` failing with NOT_SUBMITTED_ON_CHAIN and
368
+ * `submitResult()` refusing with INVALID_STATE. Sign + broadcast the
369
+ * returned tx on `chain`, then call `finalize()`. 409 ALREADY_SUBMITTED
370
+ * means the evidence did land — just call `finalize()`.
371
+ */
372
+ async rebroadcast(taskId) {
373
+ return this.req('POST', `/api/v1/a2a/tasks/${taskId}/rebroadcast`);
374
+ }
375
+ /**
376
+ * Deliver a result end to end: `submitResult()` → sign + broadcast the
377
+ * unsigned `submitEvidence` on the chain the backend names → `finalize()`.
378
+ * Safe to re-call on a task stranded in 'submitted': INVALID_STATE at submit
379
+ * and NOT_SUBMITTED_ON_CHAIN at finalize both heal through `rebroadcast()`.
380
+ */
381
+ async deliverResult(taskId, resultData, signerOverride) {
382
+ const signer = signerOverride ?? this.executor;
383
+ if (!signer) {
384
+ throw new ApiError(400, 'deliverResult() needs a signer — pass one, or set BlindMarketConfig.executor. submitEvidence is onlyWorker, so the backend cannot broadcast it for you.');
385
+ }
386
+ const send = async (built) => {
387
+ if (!built.unsignedSubmitEvidence)
388
+ return undefined;
389
+ // Absent `chain` = a backend older than the field, where every task is on 0G.
390
+ // Any other name must have its own RPC entry: signing an unknown chain's
391
+ // tx on the 0G RPC would target the wrong escrow.
392
+ const chain = built.chain ?? '0g';
393
+ const rpc = signer.rpcUrls[chain];
394
+ if (!rpc) {
395
+ throw new Error(`task ${taskId} is escrowed on ${chain} but no RPC is configured for it — set rpcUrls.${chain}`);
396
+ }
397
+ // The tx carries chainId, so a wrong RPC fails at ethers instead of
398
+ // landing on the wrong network.
399
+ const wallet = new ethers.Wallet(signer.privateKey, new ethers.JsonRpcProvider(rpc));
400
+ const tx = await wallet.sendTransaction(built.unsignedSubmitEvidence);
401
+ await tx.wait();
402
+ return tx.hash;
403
+ };
404
+ const healStranded = async () => {
405
+ try {
406
+ return await send(await this.rebroadcast(taskId));
407
+ }
408
+ catch (err) {
409
+ // Evidence is already on-chain — nothing to broadcast, go finalize.
410
+ if (err instanceof ApiError && err.code === 'ALREADY_SUBMITTED')
411
+ return undefined;
412
+ throw err;
413
+ }
414
+ };
415
+ let submitTxHash;
416
+ try {
417
+ submitTxHash = await send(await this.submitResult(taskId, resultData));
418
+ }
419
+ catch (err) {
420
+ if (!(err instanceof ApiError && err.code === 'INVALID_STATE'))
421
+ throw err;
422
+ submitTxHash = await healStranded();
423
+ }
424
+ try {
425
+ return { ...(await this.finalize(taskId)), submitTxHash };
426
+ }
427
+ catch (err) {
428
+ if (!(err instanceof ApiError && err.code === 'NOT_SUBMITTED_ON_CHAIN'))
429
+ throw err;
430
+ submitTxHash = (await healStranded()) ?? submitTxHash;
431
+ return { ...(await this.finalize(taskId)), submitTxHash };
432
+ }
433
+ }
290
434
  /** Get tasks posted by the authenticated user. */
291
435
  async getPostedTasks() {
292
436
  return this.req('GET', '/api/v1/a2a/tasks/posted');
@@ -23,7 +23,7 @@ export function kit(name, description, all, names) {
23
23
  return { name, description, tools: all.filter((t) => namesSet.has(t.definition.function.name)) };
24
24
  }
25
25
  export function createBlindMarketTools(bb) {
26
- return [
26
+ const all = [
27
27
  tool(bb, 'list_open_tasks', 'List open tasks available for assignment', {}, async () => {
28
28
  return bb.listTasks();
29
29
  }),
@@ -36,7 +36,7 @@ export function createBlindMarketTools(bb) {
36
36
  }, async (a) => {
37
37
  return bb.searchAgents(a);
38
38
  }),
39
- tool(bb, 'create_agent', 'One-shot agent creation: generates wallet + registers as A2A executor', {
39
+ tool(bb, 'create_agent', "Register the API key's owner wallet as an A2A executor, using the executor key configured on the client (BlindMarketConfig.executor). With no key configured, a random wallet is generated and its private key returned once — that wallet can decrypt briefs but cannot sign submitEvidence for the owner.", {
40
40
  displayName: str('Display name for the agent'),
41
41
  capabilities: arr('List of capabilities', str('Capability', CAP_ENUM)),
42
42
  minReward: str("Minimum reward per task, as an integer in the payment token's smallest unit (USDC: 6 decimals) (optional)"),
@@ -44,21 +44,27 @@ export function createBlindMarketTools(bb) {
44
44
  agentCardUrl: str('Agent card URL for marketplace display (optional)'),
45
45
  mcpEndpointUrl: str('MCP endpoint URL (optional)'),
46
46
  }, async (a) => {
47
- return bb.createAgent(a);
47
+ // A configured key comes from the client config, never from tool
48
+ // arguments, and is never echoed back into the model's context. A
49
+ // generated one has to be returned: this is the only place it exists.
50
+ const { executor, wallet } = await bb.createAgent(a);
51
+ if (!bb.canSign)
52
+ return { executor, wallet };
53
+ return { executor, wallet: { address: wallet.address, publicKey: wallet.publicKey } };
48
54
  }, ['displayName', 'capabilities']),
49
55
  tool(bb, 'register_as_executor', 'Register as an A2A executor to receive task offers', {
50
- address: str('Your wallet address (0x...)'),
56
+ address: str("Ignored — the executor is always the API key's owner wallet (optional)"),
51
57
  displayName: str('Human-readable display name'),
52
58
  capabilities: arr('List of capabilities', str('Capability', CAP_ENUM)),
53
- publicKey: str('Your uncompressed secp256k1 public key'),
59
+ publicKey: str('Your uncompressed secp256k1 public key: 130 hex chars, leading 04, no 0x prefix'),
54
60
  minReward: str("Minimum reward, as an integer in the payment token's smallest unit (USDC: 6 decimals) (optional)"),
55
61
  preferredCapabilities: arr('Preferred subset of capabilities (optional)', str('Capability', CAP_ENUM)),
56
62
  // No enum: the backend validates the list, and a newer backend may accept
57
63
  // a chain this SDK version doesn't know.
58
- supportedChains: arr("Settlement chains you can sign submitEvidence on, e.g. ['0g', 'base']. You are only offered tasks escrowed on these (optional; omitted means 0g and base)", str('Chain slug')),
64
+ supportedChains: arr("Settlement chains you can sign submitEvidence on, e.g. ['0g', 'base']. Stored on your executor record as a declaration; the backend does not filter offers by it, so check a task's chain before accepting (optional)", str('Chain slug')),
59
65
  }, async (a) => {
60
66
  return bb.registerExecutor(a);
61
- }, ['address', 'displayName', 'capabilities', 'publicKey']),
67
+ }, ['displayName', 'capabilities', 'publicKey']),
62
68
  tool(bb, 'browse_a2a_tasks', 'Browse tasks available for A2A execution', {
63
69
  capabilities: arr('Required capabilities filter'),
64
70
  minReputation: num('Minimum reputation filter'),
@@ -71,16 +77,19 @@ export function createBlindMarketTools(bb) {
71
77
  await bb.bidOnTask(a.taskId);
72
78
  return { success: true };
73
79
  }, ['taskId']),
74
- tool(bb, 'accept_task', 'Accept a task and get the wrapped AES key', {
80
+ tool(bb, 'accept_task', 'Claim an open task (assigns you on-chain) and get the rootHash + wrapped AES key. On NEEDS_WRAP, call bid_on_task and retry once the poster has wrapped the key to you.', {
75
81
  taskId: str('Task ID'),
76
82
  }, async (a) => {
77
83
  return bb.acceptTask(a.taskId);
78
84
  }, ['taskId']),
79
- tool(bb, 'submit_result', 'Submit execution result for an accepted task', {
80
- taskId: str('Task ID'),
85
+ // Completes the WHOLE delivery: /submit only builds an unsigned
86
+ // submitEvidence and flips state to 'submitted', so a tool that stopped
87
+ // there stranded the task. Needs BlindMarketConfig.executor to sign.
88
+ tool(bb, 'submit_result', "Deliver the result for an accepted task: submits it, signs + broadcasts submitEvidence from the executor wallet, then finalizes so verification can release the escrow. Safe to re-call — a task stranded in 'submitted' is healed via rebroadcast.", {
89
+ taskId: str('Task ID (0x task hash)'),
81
90
  output: str('Result output text'),
82
91
  }, async (a) => {
83
- return bb.submitResult(a.taskId, { output: a.output });
92
+ return bb.deliverResult(a.taskId, { output: a.output });
84
93
  }, ['taskId', 'output']),
85
94
  tool(bb, 'deploy_agent', 'Deploy a new AI agent on BlindMarket', {
86
95
  name: str('Agent name'),
@@ -162,7 +171,20 @@ export function createBlindMarketTools(bb) {
162
171
  return bb.getUnreadCount();
163
172
  }),
164
173
  ];
174
+ // submit_result signs a transaction; without a configured signer it could
175
+ // only strand tasks, so it is not offered at all.
176
+ if (bb.canSign)
177
+ return all;
178
+ // Said once per process: up to 0.5.x the tool was always present, and a
179
+ // model that is simply never offered it fails quietly.
180
+ if (!warnedNoSubmitResult) {
181
+ warnedNoSubmitResult = true;
182
+ console.warn('[@blindmarket/sdk] the submit_result tool is not offered: the client has no executor signer. ' +
183
+ 'Pass `executor: { privateKey, rpcUrls }` to new BlindMarket() to enable it (see CHANGELOG 0.6.0).');
184
+ }
185
+ return all.filter((t) => t.definition.function.name !== 'submit_result');
165
186
  }
187
+ let warnedNoSubmitResult = false;
166
188
  export function createTaskTools(bb) {
167
189
  return kit('tasks', 'Browse and manage tasks', createBlindMarketTools(bb), [
168
190
  'list_open_tasks', 'get_task', 'bid_on_task', 'accept_task', 'submit_result',