@blindmarket/sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ export declare const networks: {
23
23
  readonly name: "0g-mainnet";
24
24
  readonly chainType: "evm";
25
25
  readonly chainId: 16661;
26
- readonly rpc: ["https://evmrpc.0g.ai"];
26
+ readonly rpc: ["https://0g-rpc.publicnode.com"];
27
27
  readonly contracts: {
28
28
  readonly escrow: "0x3d0374963DaaD43e31d42373eb11156A8e8ce2Ff";
29
29
  readonly registry: "0x9CCF9c196006B573FaA9C9c9CebDd1296dbd5cE0";
@@ -19,7 +19,7 @@ export const networks = {
19
19
  name: '0g-mainnet',
20
20
  chainType: 'evm',
21
21
  chainId: 16661,
22
- rpc: ['https://evmrpc.0g.ai'],
22
+ rpc: ['https://0g-rpc.publicnode.com'],
23
23
  contracts: {
24
24
  escrow: '0x3d0374963DaaD43e31d42373eb11156A8e8ce2Ff',
25
25
  registry: '0x9CCF9c196006B573FaA9C9c9CebDd1296dbd5cE0',
package/dist/onchain.d.ts CHANGED
@@ -13,12 +13,16 @@ import { ethers } from 'ethers';
13
13
  export declare const DEFAULT_CONFIRM_TIMEOUT_MS = 180000;
14
14
  export interface SendOptions {
15
15
  /**
16
- * Called with the hash as soon as the transaction is broadcast, before any
17
- * wait, and again with the replacement's hash if the wallet re-prices it.
18
- * Persist it there: a crash from then on cannot lose it. A throwing callback
19
- * does not stop the send, which has already happened.
16
+ * Called with the hash and nonce as soon as the transaction is broadcast,
17
+ * before any wait, and again with the replacement's if the wallet re-prices
18
+ * it. Persist them there: a crash from then on cannot lose them, and the
19
+ * nonce tells later whether the transaction can still land (once the
20
+ * sender's confirmed nonce passes it with no receipt, it never will). A
21
+ * local key's signed raw transaction comes too: re-broadcast while its nonce
22
+ * is unused, it can only land once, with this hash. A throwing callback does
23
+ * not stop the send, which has already happened.
20
24
  */
21
- onSent?: (hash: string) => void | Promise<void>;
25
+ onSent?: (hash: string, nonce: number, raw?: string) => void | Promise<void>;
22
26
  timeoutMs?: number;
23
27
  nonce?: number;
24
28
  value?: bigint;
package/dist/onchain.js CHANGED
@@ -21,11 +21,11 @@ export class UnconfirmedTransactionError extends Error {
21
21
  this.name = 'UnconfirmedTransactionError';
22
22
  }
23
23
  }
24
- async function notify(onSent, hash) {
24
+ async function notify(onSent, hash, nonce, raw) {
25
25
  if (!onSent)
26
26
  return;
27
27
  try {
28
- await onSent(hash);
28
+ await onSent(hash, nonce, raw);
29
29
  }
30
30
  catch {
31
31
  // The transaction is out whatever the callback did; the caller still gets the hash.
@@ -38,14 +38,35 @@ async function notify(onSent, hash) {
38
38
  * not be confirmed throws UnconfirmedTransactionError naming the hash.
39
39
  */
40
40
  export async function sendAndWait(signer, tx, opts = {}) {
41
- const sent = await signer.sendTransaction({
41
+ const request = {
42
42
  to: tx.to,
43
43
  data: tx.data,
44
44
  ...(opts.value !== undefined ? { value: opts.value } : {}),
45
45
  ...(opts.nonce !== undefined ? { nonce: opts.nonce } : {}),
46
46
  ...(opts.gasLimit !== undefined ? { gasLimit: opts.gasLimit } : {}),
47
- });
48
- await notify(opts.onSent, sent.hash);
47
+ };
48
+ let sent;
49
+ if (signsLocally(signer)) {
50
+ // Signed, and its hash and nonce handed to onSent, before it leaves this
51
+ // process: once the raw transaction is out, a lost reply cannot turn one
52
+ // that may land into "nothing was sent".
53
+ const populated = await signer.populateTransaction(request);
54
+ const raw = await signer.signTransaction(populated);
55
+ const hash = ethers.keccak256(raw);
56
+ await notify(opts.onSent, hash, Number(populated.nonce), raw);
57
+ try {
58
+ sent = await signer.provider.broadcastTransaction(raw);
59
+ }
60
+ catch (err) {
61
+ const hint = opts.unconfirmedHint ? ` ${opts.unconfirmedHint(hash)}` : '';
62
+ throw new UnconfirmedTransactionError(hash, `Transaction ${hash} was signed and handed to the node, but no answer came back (${err.message}). It may still land.${hint}`);
63
+ }
64
+ }
65
+ else {
66
+ // A wallet that signs out of reach (a browser extension) signs and sends in one step.
67
+ sent = await signer.sendTransaction(request);
68
+ await notify(opts.onSent, sent.hash, sent.nonce);
69
+ }
49
70
  try {
50
71
  const receipt = await sent.wait(1, opts.timeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS);
51
72
  if (receipt && receipt.status === 0)
@@ -59,7 +80,7 @@ export async function sendAndWait(signer, tx, opts = {}) {
59
80
  const replacement = err.replacement;
60
81
  if (err.receipt && err.receipt.status === 0)
61
82
  throw new Error(`Transaction ${replacement.hash} reverted, so nothing was paid.`);
62
- await notify(opts.onSent, replacement.hash);
83
+ await notify(opts.onSent, replacement.hash, replacement.nonce);
63
84
  return { hash: replacement.hash, nonce: replacement.nonce };
64
85
  }
65
86
  if (ethers.isError(err, 'CALL_EXCEPTION'))
@@ -70,6 +91,10 @@ export async function sendAndWait(signer, tx, opts = {}) {
70
91
  throw new UnconfirmedTransactionError(sent.hash, `Transaction ${sent.hash} was sent but not confirmed (${err.message}).${hint}`);
71
92
  }
72
93
  }
94
+ /** A signer holding its own key, with a provider: its transactions can be signed, recorded, then broadcast. */
95
+ function signsLocally(signer) {
96
+ return signer instanceof ethers.BaseWallet && !!signer.provider;
97
+ }
73
98
  /**
74
99
  * Throw, before anything is sent, unless `signer` is connected to chain
75
100
  * `chainId`. A transaction signed on the wrong network can succeed there
@@ -0,0 +1,63 @@
1
+ import type { Address, AgentCapability, CreateTaskRequest } from './types.js';
2
+ import type { IndexTaskParams, PostTaskParams } from './index.js';
3
+ /** The escrow's deadline bounds (BlindEscrow MIN_DEADLINE / MAX_DEADLINE). */
4
+ export declare const MIN_DURATION_SECONDS = 3600;
5
+ export declare const MAX_DURATION_SECONDS: number;
6
+ /** A brief's key is wrapped to at most this many executors (POST /tasks caps wrappedKeys at 200). */
7
+ export declare const MAX_WRAPPED_KEYS = 200;
8
+ /** A whole number of base units from a string or bigint; throws 400 INVALID_AMOUNT otherwise. */
9
+ export declare function wholeNumber(value: string | bigint, name: string): bigint;
10
+ /** A post with every default filled in and every limit checked. */
11
+ export interface NormalizedPost {
12
+ instructions: string;
13
+ amount: bigint;
14
+ duration: number;
15
+ privacy: 'private' | 'public';
16
+ locationZone: string;
17
+ verificationMode: 'manual' | 'auto' | 'agent';
18
+ verificationCriteria?: Record<string, unknown>;
19
+ requiredCapabilities: AgentCapability[];
20
+ verifierAddress?: Address;
21
+ targetExecutor?: Address;
22
+ routingSummary?: string;
23
+ }
24
+ /** POST /a2a/tasks/index takes a routing summary of at most this many characters. */
25
+ export declare const MAX_ROUTING_SUMMARY = 500;
26
+ /**
27
+ * `params` with its defaults, or the ApiError postTask() has always thrown,
28
+ * in the same order: INVALID_AMOUNT, AMOUNT_ABOVE_MAX, INVALID_DURATION.
29
+ */
30
+ export declare function normalizePost(params: PostTaskParams, maxAmountRaw?: bigint | string): NormalizedPost;
31
+ /**
32
+ * The checks a bulk post adds for fields a caller builds from a file or a
33
+ * form, where postTask()'s TypeScript types do not reach: a brief with text,
34
+ * the enums, and the addresses. Throws 400 INVALID_ROW naming the field.
35
+ */
36
+ export declare function checkRowFields(params: PostTaskParams): void;
37
+ /** An executor as GET /a2a/executors lists it. */
38
+ export interface ExecutorKey {
39
+ address: string;
40
+ publicKey?: string;
41
+ }
42
+ /** A brief ready to upload: the bytes, their task hash, and for a private brief its wrapped keys. */
43
+ export interface SealedBrief {
44
+ blob: Uint8Array;
45
+ /** 0x + sha256 of `blob`: the commitment createTask escrows against. */
46
+ taskHash: string;
47
+ wrappedKeys?: Record<string, string>;
48
+ /** Hex AES key of a private brief. */
49
+ aesKey?: string;
50
+ }
51
+ /**
52
+ * The brief as it is uploaded: plaintext for a public post; otherwise
53
+ * AES-encrypted, with the key wrapped to each executor that can take it (or
54
+ * only the target executor). Throws what postTask() has always thrown, with
55
+ * nothing sent: EXECUTOR_NOT_FOUND, TOO_MANY_EXECUTORS, NO_EXECUTORS.
56
+ */
57
+ export declare function sealBrief(post: NormalizedPost, executors: readonly ExecutorKey[], postingChain: string): Promise<SealedBrief>;
58
+ /** POST /api/v1/tasks's body for one post (POST /tasks/batch takes the same, less the token). */
59
+ export declare function createTaskBody(post: NormalizedPost, sealed: SealedBrief, token: string, rootHash: string): CreateTaskRequest;
60
+ /** POST /api/v1/a2a/tasks/index's body for one post, with `txHash` still to fill in. */
61
+ export declare function indexParamsFor(post: NormalizedPost, sealed: SealedBrief, rootHash: string): IndexTaskParams;
62
+ /** Whether this post commits a verifier on-chain (createTaskWithVerifier, or a non-zero verifierAgent in createTasks). */
63
+ export declare function commitsVerifier(post: NormalizedPost): boolean;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The pieces of a post that need no network: a post's fields with their
3
+ * defaults and the escrow's limits, the sealed brief and its task hash, and
4
+ * the bodies POST /tasks and POST /a2a/tasks/index take. postTask() and
5
+ * postTasks() share them, so one task and five hundred are checked the same
6
+ * way, and every row of a bulk post is sealed before anything is sent.
7
+ */
8
+ import { ethers } from 'ethers';
9
+ import { ApiError } from './apiError.js';
10
+ import { generateAesKey, aesEncrypt, eciesEncrypt, sha256, bytesToHex } from './crypto/index.js';
11
+ /** The escrow's deadline bounds (BlindEscrow MIN_DEADLINE / MAX_DEADLINE). */
12
+ export const MIN_DURATION_SECONDS = 3_600;
13
+ export const MAX_DURATION_SECONDS = 90 * 86_400;
14
+ /** A brief's key is wrapped to at most this many executors (POST /tasks caps wrappedKeys at 200). */
15
+ export const MAX_WRAPPED_KEYS = 200;
16
+ /** A whole number of base units from a string or bigint; throws 400 INVALID_AMOUNT otherwise. */
17
+ export function wholeNumber(value, name) {
18
+ if (typeof value === 'bigint')
19
+ return value;
20
+ if (typeof value === 'string' && /^\d+$/.test(value))
21
+ return BigInt(value);
22
+ throw new ApiError(400, `${name} must be a whole number of the token's smallest unit (USDC has 6 decimals: '2500000' is 2.5 USDC), not ${JSON.stringify(value)}. Nothing was sent.`, undefined, 'INVALID_AMOUNT');
23
+ }
24
+ /** POST /a2a/tasks/index takes a routing summary of at most this many characters. */
25
+ export const MAX_ROUTING_SUMMARY = 500;
26
+ /**
27
+ * `params` with its defaults, or the ApiError postTask() has always thrown,
28
+ * in the same order: INVALID_AMOUNT, AMOUNT_ABOVE_MAX, INVALID_DURATION.
29
+ */
30
+ export function normalizePost(params, maxAmountRaw) {
31
+ const amount = wholeNumber(params.amountRaw, 'amountRaw');
32
+ if (amount <= 0n)
33
+ throw new ApiError(400, 'amountRaw must be above 0. Nothing was sent.', undefined, 'INVALID_AMOUNT');
34
+ if (maxAmountRaw !== undefined && amount > BigInt(maxAmountRaw)) {
35
+ throw new ApiError(402, `The escrow of ${amount} is above your limit of ${maxAmountRaw}. Nothing was sent.`, undefined, 'AMOUNT_ABOVE_MAX');
36
+ }
37
+ const duration = params.durationSeconds ?? 86_400;
38
+ if (!Number.isInteger(duration) || duration < MIN_DURATION_SECONDS || duration > MAX_DURATION_SECONDS) {
39
+ throw new ApiError(400, 'durationSeconds must be a whole number from 3600 (1 hour) to 7776000 (90 days): the escrow refuses anything else. Nothing was sent.', undefined, 'INVALID_DURATION');
40
+ }
41
+ const verificationMode = params.verificationMode ?? 'auto';
42
+ const verificationCriteria = params.verificationCriteria
43
+ ?? (verificationMode === 'auto' ? { min_length: 10, pass_threshold: 60 } : undefined);
44
+ // Checked here, before funding: the listing would refuse it after the escrow is paid.
45
+ const routingSummary = typeof params.routingSummary === 'string' ? params.routingSummary.trim() : undefined;
46
+ if (routingSummary && routingSummary.length > MAX_ROUTING_SUMMARY) {
47
+ throw new ApiError(400, `routingSummary is ${routingSummary.length} characters; the task board takes at most ${MAX_ROUTING_SUMMARY}. Nothing was sent.`, undefined, 'INVALID_ROUTING_SUMMARY');
48
+ }
49
+ return {
50
+ instructions: params.instructions,
51
+ amount,
52
+ duration,
53
+ privacy: params.privacy ?? 'private',
54
+ locationZone: params.locationZone ?? 'global',
55
+ verificationMode,
56
+ ...(verificationCriteria ? { verificationCriteria } : {}),
57
+ requiredCapabilities: params.requiredCapabilities ?? [],
58
+ ...(params.verifierAddress ? { verifierAddress: params.verifierAddress } : {}),
59
+ ...(params.targetExecutor ? { targetExecutor: params.targetExecutor } : {}),
60
+ ...(routingSummary ? { routingSummary } : {}),
61
+ };
62
+ }
63
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
64
+ /**
65
+ * The checks a bulk post adds for fields a caller builds from a file or a
66
+ * form, where postTask()'s TypeScript types do not reach: a brief with text,
67
+ * the enums, and the addresses. Throws 400 INVALID_ROW naming the field.
68
+ */
69
+ export function checkRowFields(params) {
70
+ const bad = (message) => new ApiError(400, `${message} Nothing was sent.`, undefined, 'INVALID_ROW');
71
+ if (typeof params.instructions !== 'string' || !params.instructions.trim())
72
+ throw bad('instructions are empty.');
73
+ if (params.privacy !== undefined && params.privacy !== 'private' && params.privacy !== 'public') {
74
+ throw bad(`privacy must be 'private' or 'public', not ${JSON.stringify(params.privacy)}.`);
75
+ }
76
+ if (params.verificationMode !== undefined && !['manual', 'auto', 'agent'].includes(params.verificationMode)) {
77
+ throw bad(`verificationMode must be 'auto', 'manual' or 'agent', not ${JSON.stringify(params.verificationMode)}.`);
78
+ }
79
+ if (params.targetExecutor !== undefined && !ADDRESS.test(params.targetExecutor)) {
80
+ throw bad(`targetExecutor must be a 0x wallet address, not ${JSON.stringify(params.targetExecutor)}.`);
81
+ }
82
+ if (params.verifierAddress !== undefined && !ADDRESS.test(params.verifierAddress)) {
83
+ throw bad(`verifierAddress must be a 0x wallet address, not ${JSON.stringify(params.verifierAddress)}.`);
84
+ }
85
+ if (params.requiredCapabilities !== undefined && (!Array.isArray(params.requiredCapabilities) || params.requiredCapabilities.some((c) => typeof c !== 'string'))) {
86
+ throw bad('requiredCapabilities must be a list of capability names.');
87
+ }
88
+ if (params.durationSeconds !== undefined && typeof params.durationSeconds !== 'number') {
89
+ throw bad('durationSeconds must be a number of seconds.');
90
+ }
91
+ if (params.routingSummary !== undefined && typeof params.routingSummary !== 'string') {
92
+ throw bad('routingSummary must be text.');
93
+ }
94
+ }
95
+ /**
96
+ * The brief as it is uploaded: plaintext for a public post; otherwise
97
+ * AES-encrypted, with the key wrapped to each executor that can take it (or
98
+ * only the target executor). Throws what postTask() has always thrown, with
99
+ * nothing sent: EXECUTOR_NOT_FOUND, TOO_MANY_EXECUTORS, NO_EXECUTORS.
100
+ */
101
+ export async function sealBrief(post, executors, postingChain) {
102
+ const plaintext = new TextEncoder().encode(post.instructions);
103
+ if (post.privacy === 'public') {
104
+ return { blob: plaintext, taskHash: `0x${bytesToHex(await sha256(plaintext))}` };
105
+ }
106
+ let targets = executors.filter((e) => typeof e.publicKey === 'string' && e.publicKey.length > 0);
107
+ if (post.targetExecutor) {
108
+ const want = post.targetExecutor.toLowerCase();
109
+ targets = targets.filter((e) => e.address.toLowerCase() === want);
110
+ if (targets.length === 0) {
111
+ throw new ApiError(404, `${post.targetExecutor} is not a registered executor on ${postingChain} with a public key, so it could not read the brief. Nothing was sent.`, undefined, 'EXECUTOR_NOT_FOUND');
112
+ }
113
+ }
114
+ if (targets.length > MAX_WRAPPED_KEYS) {
115
+ throw new ApiError(409, `${targets.length} executors match, more than the ${MAX_WRAPPED_KEYS} a brief can be wrapped to. Narrow requiredCapabilities, name a targetExecutor, or post with privacy 'public'. Nothing was sent.`, undefined, 'TOO_MANY_EXECUTORS');
116
+ }
117
+ const key = await generateAesKey();
118
+ const blob = await aesEncrypt(plaintext, key);
119
+ const wrappedKeys = {};
120
+ for (const e of targets) {
121
+ try {
122
+ wrappedKeys[e.address.toLowerCase()] = bytesToHex(await eciesEncrypt(key, e.publicKey));
123
+ }
124
+ catch { /* a malformed public key: that executor can't be wrapped to */ }
125
+ }
126
+ if (Object.keys(wrappedKeys).length === 0) {
127
+ throw new ApiError(409, `No executor on ${postingChain} can decrypt an encrypted brief right now, so no one could take the task. Post with privacy 'public', or wait for executors to register. Nothing was sent.`, undefined, 'NO_EXECUTORS');
128
+ }
129
+ return { blob, taskHash: `0x${bytesToHex(await sha256(blob))}`, wrappedKeys, aesKey: bytesToHex(key) };
130
+ }
131
+ /** POST /api/v1/tasks's body for one post (POST /tasks/batch takes the same, less the token). */
132
+ export function createTaskBody(post, sealed, token, rootHash) {
133
+ return {
134
+ taskHash: sealed.taskHash,
135
+ token: token,
136
+ amount: post.amount.toString(),
137
+ locationZone: post.locationZone,
138
+ duration: String(post.duration),
139
+ targetExecutorType: 'agent',
140
+ verificationMode: post.verificationMode,
141
+ ...(post.verificationCriteria ? { verificationCriteria: post.verificationCriteria } : {}),
142
+ ...(post.verifierAddress ? { verifierAddress: post.verifierAddress } : {}),
143
+ requiredCapabilities: post.requiredCapabilities,
144
+ rootHash: rootHash,
145
+ ...(sealed.wrappedKeys ? { wrappedKeys: sealed.wrappedKeys } : {}),
146
+ };
147
+ }
148
+ /** POST /api/v1/a2a/tasks/index's body for one post, with `txHash` still to fill in. */
149
+ export function indexParamsFor(post, sealed, rootHash) {
150
+ return {
151
+ txHash: '',
152
+ taskHash: sealed.taskHash,
153
+ rootHash,
154
+ ...(sealed.wrappedKeys ? { wrappedKeys: sealed.wrappedKeys } : {}),
155
+ privacy: post.privacy,
156
+ ...(post.privacy === 'public' ? { publicBrief: post.instructions.slice(0, 4000) } : {}),
157
+ verificationMode: post.verificationMode,
158
+ ...(post.verificationCriteria ? { verificationCriteria: post.verificationCriteria } : {}),
159
+ ...(post.verifierAddress ? { verifierAddress: post.verifierAddress } : {}),
160
+ requiredCapabilities: post.requiredCapabilities,
161
+ ...(post.targetExecutor ? { targetExecutor: post.targetExecutor } : {}),
162
+ ...(post.routingSummary ? { routingSummary: post.routingSummary } : {}),
163
+ };
164
+ }
165
+ /** Whether this post commits a verifier on-chain (createTaskWithVerifier, or a non-zero verifierAgent in createTasks). */
166
+ export function commitsVerifier(post) {
167
+ return post.verificationMode === 'agent' && !!post.verifierAddress && post.verifierAddress.toLowerCase() !== ethers.ZeroAddress;
168
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The escrow and settlement token of each deployment this SDK funds, by chain
3
+ * id. postTask() and postTasks() approve and fund only these: otherwise both
4
+ * addresses come from the backend's /health/settlement, and postTasks()
5
+ * approves a whole run at once, so a backend that named another "escrow"
6
+ * would be handed that approval (security review). A custom or local
7
+ * deployment is added through BlindMarketConfig.trustedEscrows.
8
+ *
9
+ * Separate from network/presets.ts, which pins the 0G contracts of the
10
+ * low-level API per SDK version (its testnet entries deliberately point at
11
+ * an older deployment). These follow contracts/deployments/arc-*.json.
12
+ */
13
+ export interface SettlementPin {
14
+ chainId: number;
15
+ escrow: string;
16
+ token: string;
17
+ }
18
+ export declare const SETTLEMENT_PINS: readonly SettlementPin[];
19
+ /** Whether `escrow` and `token` on `chainId` are a pinned deployment, or one the caller trusts. */
20
+ export declare function isPinnedSettlement(chainId: number, escrow: string, token: string, trusted?: readonly SettlementPin[]): boolean;
@@ -0,0 +1,11 @@
1
+ export const SETTLEMENT_PINS = Object.freeze([
2
+ // Arc mainnet
3
+ { chainId: 5042, escrow: '0xd2B819B57a9568Cb6bFc98C687F9a851EC8330C4', token: '0x3600000000000000000000000000000000000000' },
4
+ // Arc Testnet
5
+ { chainId: 5042002, escrow: '0xaBf70843E0380F1e749d2b85C30dD6820Ff5C731', token: '0x3600000000000000000000000000000000000000' },
6
+ ]);
7
+ /** Whether `escrow` and `token` on `chainId` are a pinned deployment, or one the caller trusts. */
8
+ export function isPinnedSettlement(chainId, escrow, token, trusted = []) {
9
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
10
+ return [...SETTLEMENT_PINS, ...trusted].some((p) => p.chainId === chainId && same(p.escrow, escrow) && same(p.token, token));
11
+ }
@@ -24,7 +24,7 @@ export function kit(name, description, all, names) {
24
24
  }
25
25
  export function createBlindMarketTools(bb) {
26
26
  const all = [
27
- tool(bb, 'list_open_tasks', 'List open tasks available for assignment', {}, async () => {
27
+ tool(bb, 'list_open_tasks', 'List open tasks from the legacy 0G TaskRegistry (numeric ids on the 0G escrow). Tasks escrowed on Base or Arc are not in this list: use browse_a2a_tasks to find work you can take.', {}, async () => {
28
28
  return bb.listTasks();
29
29
  }),
30
30
  tool(bb, 'get_task', 'Get full task details by ID', { taskId: str('Numeric or 0x task ID') }, async (a) => {
package/dist/types.d.ts CHANGED
@@ -181,6 +181,19 @@ export interface A2APublicTaskMeta {
181
181
  privacy?: 'public';
182
182
  hasEncryptedBrief?: boolean;
183
183
  rootHash?: string;
184
+ /**
185
+ * The escrowed reward, recorded by the backend from the verified TaskCreated
186
+ * event at /tasks/index: `amount` is a whole number of the unit's base units
187
+ * (USDC: 6 decimals, so '1000000' is 1 USDC). Absent on tasks indexed by a
188
+ * backend older than the field. WorkerRuntime compares it with `minReward`.
189
+ */
190
+ reward?: {
191
+ amount: string;
192
+ unit: {
193
+ symbol: 'USDC' | '0G';
194
+ decimals: 6 | 18;
195
+ };
196
+ };
184
197
  [key: string]: unknown;
185
198
  }
186
199
  /** One entry of `GET /api/v1/a2a/tasks` (and /tasks/posted, /executions). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blindmarket/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "BlindMarket SDK — deploy agents, assign workers, verify evidence",
5
5
  "author": "BlindMarket Team",
6
6
  "license": "MIT",