@blindmarket/mcp-server 0.4.0 → 0.5.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.
- package/README.md +54 -17
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/rent.d.ts.map +1 -1
- package/dist/rent.js +348 -59
- package/dist/rent.js.map +1 -1
- package/dist/settlement.d.ts +41 -2
- package/dist/settlement.d.ts.map +1 -1
- package/dist/settlement.js +70 -7
- package/dist/settlement.js.map +1 -1
- package/dist/state.d.ts +6 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +5 -3
- package/dist/wallet.js.map +1 -1
- package/package.json +1 -1
package/dist/rent.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { Contract, Interface, formatUnits, parseUnits } from 'ethers';
|
|
3
|
-
import { aesDecrypt, aesEncrypt, eciesDecrypt, eciesEncrypt, generateAesKey, sha256Hex } from './crypto.js';
|
|
2
|
+
import { Contract, Interface, JsonRpcProvider, formatUnits, parseUnits } from 'ethers';
|
|
3
|
+
import { aesDecrypt, aesEncrypt, derivePublicKeyHex, eciesDecrypt, eciesEncrypt, generateAesKey, sha256Hex } from './crypto.js';
|
|
4
4
|
import { createQuote, consumeQuote, getSpend, putSpend, updateSpend } from './state.js';
|
|
5
|
-
import { createSettlementResolver } from './settlement.js';
|
|
5
|
+
import { createSettlementResolver, isErc20Settlement, rpcFor, rpcEnvName, } from './settlement.js';
|
|
6
6
|
// Read-only view of BlindEscrow.getTask, for reading a task's state directly
|
|
7
7
|
// from the chain that holds it. Field order matches contracts/BlindEscrow.sol;
|
|
8
8
|
// a post-#38 deployment appends disputedAt, which ABI decoding ignores.
|
|
@@ -93,11 +93,16 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
93
93
|
}
|
|
94
94
|
return json.data;
|
|
95
95
|
}
|
|
96
|
-
const settlement = createSettlementResolver({
|
|
96
|
+
const settlement = createSettlementResolver({
|
|
97
|
+
apiBase: cfg.apiBase ?? 'https://api.blindmarket.xyz',
|
|
98
|
+
api,
|
|
99
|
+
localWallet: walletCtx?.wallet.address,
|
|
100
|
+
});
|
|
97
101
|
/** How this process pays. On 0G that is the local wallet, which must exist.
|
|
98
102
|
* On a relay chain (Base) nothing signs locally — the relay signs from the
|
|
99
103
|
* API key's owner wallet — so a missing BLINDMARKET_PRIVATE_KEY is not an
|
|
100
|
-
* error there.
|
|
104
|
+
* error there. On an ERC-20 chain without a relay (Arc) discovery only
|
|
105
|
+
* succeeds with the local wallet, already checked to be the key's owner. */
|
|
101
106
|
async function requireFunding() {
|
|
102
107
|
let s;
|
|
103
108
|
try {
|
|
@@ -106,7 +111,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
106
111
|
catch (err) {
|
|
107
112
|
return { error: fail(err.code ?? 'SETTLEMENT_UNKNOWN', err.message) };
|
|
108
113
|
}
|
|
109
|
-
if (s
|
|
114
|
+
if (isErc20Settlement(s))
|
|
110
115
|
return { s, payFrom: s.payFrom };
|
|
111
116
|
if (!walletCtx) {
|
|
112
117
|
return { error: fail('NO_WALLET', 'Spending on 0G needs a local funding wallet — set BLINDMARKET_PRIVATE_KEY (see wallet_status)') };
|
|
@@ -124,10 +129,23 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
124
129
|
/** A new escrow is funded where POST /api/v1/tasks builds: the backend's
|
|
125
130
|
* posting chain. A process forced onto another chain (to finish or refund
|
|
126
131
|
* tasks already there) cannot post. Unknown on an older backend. */
|
|
127
|
-
function notPostingChain(s) {
|
|
128
|
-
|
|
132
|
+
async function notPostingChain(s) {
|
|
133
|
+
let posting = s.postingChain;
|
|
134
|
+
// Forced to 0G, discovery never asked the backend. Ask now, before a quote:
|
|
135
|
+
// a backend that posts elsewhere refuses the native-0G createTask only
|
|
136
|
+
// after the brief is uploaded and its hash claimed.
|
|
137
|
+
if (posting === undefined && s.mode === '0g') {
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${cfg.apiBase}/health/settlement`, { signal: AbortSignal.timeout(15_000) });
|
|
140
|
+
const json = await res.json();
|
|
141
|
+
if (json?.success && typeof json.data?.postingChain === 'string')
|
|
142
|
+
posting = json.data.postingChain;
|
|
143
|
+
}
|
|
144
|
+
catch { /* an older backend or a blip: keep the old behaviour, the escrow check still guards the send */ }
|
|
145
|
+
}
|
|
146
|
+
if (posting === undefined || s.mode === posting)
|
|
129
147
|
return null;
|
|
130
|
-
const e = new Error(`This process settles on ${s.mode} (BLINDMARKET_SETTLEMENT), but the backend posts new tasks on ${
|
|
148
|
+
const e = new Error(`This process settles on ${s.mode} (BLINDMARKET_SETTLEMENT), but the backend posts new tasks on ${posting}, so a new escrow can only be funded there. Unset BLINDMARKET_SETTLEMENT (or set it to ${posting}) to post; ${s.mode} stays usable for tasks already on it.`);
|
|
131
149
|
e.code = 'NOT_POSTING_CHAIN';
|
|
132
150
|
return e;
|
|
133
151
|
}
|
|
@@ -139,10 +157,15 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
139
157
|
function settlementToken(s) {
|
|
140
158
|
return new Contract(s.token.address, ERC20, s.provider);
|
|
141
159
|
}
|
|
160
|
+
/** The local wallet on a no-relay ERC-20 chain, over that chain's RPC
|
|
161
|
+
* (checked at discovery to serve the chain the backend names). */
|
|
162
|
+
function localSigner(s) {
|
|
163
|
+
return walletCtx.wallet.connect(s.provider);
|
|
164
|
+
}
|
|
142
165
|
/** Spendable balance of whoever pays, in the settlement token's units. */
|
|
143
166
|
async function payFromBalance(s, payFrom) {
|
|
144
167
|
try {
|
|
145
|
-
const raw = s
|
|
168
|
+
const raw = isErc20Settlement(s)
|
|
146
169
|
? await settlementToken(s).balanceOf(payFrom)
|
|
147
170
|
: await walletCtx.provider.getBalance(payFrom);
|
|
148
171
|
return formatUnits(raw, s.decimals);
|
|
@@ -213,6 +236,20 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
213
236
|
// default it applies to this body: user-pays.
|
|
214
237
|
return { hash: r.hash, isUserOp: r.isUserOp === true, gas: r.gas ?? 'user-pays' };
|
|
215
238
|
}
|
|
239
|
+
/** Send a tx on an ERC-20 settlement: through the relay, or signed by the
|
|
240
|
+
* local wallet on a chain the relay does not serve. Either way the caller
|
|
241
|
+
* persists the hash before waiting (waitRelayed polls the chain's own RPC
|
|
242
|
+
* for a plain hash, which a local send always is). `nonce` pins a local
|
|
243
|
+
* send right after one this process made: an RPC can answer the next nonce
|
|
244
|
+
* lookup from before the earlier tx landed. */
|
|
245
|
+
async function sendErc20(s, tx, nonce) {
|
|
246
|
+
if (s.payment === 'relay-erc20')
|
|
247
|
+
return relaySend(s, tx);
|
|
248
|
+
// No gasLimit: estimation runs first, so a predictable revert (a stale
|
|
249
|
+
// allowance, a passed deadline) fails here without being mined and paid for.
|
|
250
|
+
const sent = await localSigner(s).sendTransaction({ to: tx.to, data: tx.data, ...(nonce !== undefined ? { nonce } : {}) });
|
|
251
|
+
return { hash: sent.hash, isUserOp: false, nonce: sent.nonce };
|
|
252
|
+
}
|
|
216
253
|
/** Wait for a relayed tx to land. A plain hash can be polled for its receipt;
|
|
217
254
|
* a user-op hash cannot (getTransactionReceipt is always null for it), so
|
|
218
255
|
* that case returns at once and the caller confirms by on-chain STATE —
|
|
@@ -224,7 +261,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
224
261
|
const receipt = await s.provider.getTransactionReceipt(hash).catch(() => null);
|
|
225
262
|
if (receipt) {
|
|
226
263
|
if (receipt.status === 0) {
|
|
227
|
-
const e = new Error(`
|
|
264
|
+
const e = new Error(`tx ${hash} reverted`);
|
|
228
265
|
e.code = 'TX_REVERTED';
|
|
229
266
|
throw e;
|
|
230
267
|
}
|
|
@@ -232,21 +269,26 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
232
269
|
}
|
|
233
270
|
await new Promise((r) => setTimeout(r, 3000));
|
|
234
271
|
}
|
|
235
|
-
const e = new Error(`
|
|
272
|
+
const e = new Error(`tx ${hash} not confirmed after 90s — retry with the same idempotencyKey to resume`);
|
|
236
273
|
e.code = 'TX_PENDING';
|
|
237
274
|
throw e;
|
|
238
275
|
}
|
|
239
|
-
/**
|
|
276
|
+
/** ERC-20 chains: createTask pulls the token via transferFrom, so the
|
|
240
277
|
* escrow needs an allowance first. Confirmed by re-reading allowance()
|
|
241
|
-
* rather than by receipt, which is what makes the user-op case decidable.
|
|
278
|
+
* rather than by receipt, which is what makes the user-op case decidable.
|
|
279
|
+
* Returns the nonce the createTask should use when this call sent a local
|
|
280
|
+
* approve, else undefined. */
|
|
242
281
|
async function ensureAllowance(s, record) {
|
|
243
282
|
const need = BigInt(record.amountWei);
|
|
244
283
|
const token = settlementToken(s);
|
|
245
284
|
if ((await token.allowance(s.payFrom, s.escrowAddress)) >= need)
|
|
246
|
-
return;
|
|
285
|
+
return undefined;
|
|
286
|
+
let nextNonce;
|
|
247
287
|
const approve = async () => {
|
|
248
288
|
const data = ERC20.encodeFunctionData('approve', [s.escrowAddress, need]);
|
|
249
|
-
const { hash } = await
|
|
289
|
+
const { hash, nonce } = await sendErc20(s, { to: s.token.address, data });
|
|
290
|
+
if (nonce !== undefined)
|
|
291
|
+
nextNonce = nonce + 1;
|
|
250
292
|
// Persist BEFORE waiting: a crash here must resume into the poll below.
|
|
251
293
|
updateSpend(record.idempotencyKey, { stage: 'approved', approveTxHash: hash });
|
|
252
294
|
record.stage = 'approved';
|
|
@@ -263,18 +305,18 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
263
305
|
if (record.stage === 'created') {
|
|
264
306
|
await approve();
|
|
265
307
|
if (await settled())
|
|
266
|
-
return;
|
|
308
|
+
return nextNonce;
|
|
267
309
|
}
|
|
268
310
|
// Resumed at 'approved' (or the fresh approve never landed): the earlier
|
|
269
311
|
// approve was dropped or reverted. Sending another is safe — ERC-20
|
|
270
312
|
// approve SETS the allowance, it does not add — and it is the only way
|
|
271
313
|
// out of this stage, so do it rather than leave the record stuck.
|
|
272
314
|
if (await settled())
|
|
273
|
-
return;
|
|
315
|
+
return nextNonce;
|
|
274
316
|
await approve();
|
|
275
317
|
if (await settled())
|
|
276
|
-
return;
|
|
277
|
-
const e = new Error(`${s.symbol} allowance still below ${formatUnits(need, s.decimals)} after two approves (last ${record.approveTxHash}) — check
|
|
318
|
+
return nextNonce;
|
|
319
|
+
const e = new Error(`${s.symbol} allowance still below ${formatUnits(need, s.decimals)} after two approves (last ${record.approveTxHash}) — check ${s.payFrom}'s ${s.symbol} balance and retry with the same idempotencyKey`);
|
|
278
320
|
e.code = 'APPROVE_PENDING';
|
|
279
321
|
throw e;
|
|
280
322
|
}
|
|
@@ -288,25 +330,28 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
288
330
|
* both through the Privy relay with no local signing at all. The
|
|
289
331
|
* backend picks the escrow; we only check it is the one we approved. */
|
|
290
332
|
async function fundAndIndex(record) {
|
|
291
|
-
const s = await settlement();
|
|
292
333
|
let { txHash } = record;
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
if (record.settlement && record.settlement !== s.mode) {
|
|
297
|
-
const e = new Error(`spend ${record.idempotencyKey} started on ${record.settlement} but the backend now settles on ${s.mode} — finish or refund it from the web app`);
|
|
298
|
-
e.code = 'SETTLEMENT_CHANGED';
|
|
299
|
-
throw e;
|
|
300
|
-
}
|
|
334
|
+
// Nothing left to sign once the escrow is funded: /a2a/tasks/index finds
|
|
335
|
+
// the receipt on whichever chain holds it. So a funded spend finishes
|
|
336
|
+
// even when settlement cannot be discovered right now, or has moved.
|
|
301
337
|
if (record.stage === 'created' || record.stage === 'approved') {
|
|
302
|
-
const
|
|
338
|
+
const s = await settlement();
|
|
339
|
+
// A record remembers the chain it started on. If the backend flips mode
|
|
340
|
+
// between attempts, re-funding through the other path would double-fund
|
|
341
|
+
// or send native value into a USDC transferFrom — refuse instead.
|
|
342
|
+
if (record.settlement && record.settlement !== s.mode) {
|
|
343
|
+
const e = new Error(`spend ${record.idempotencyKey} started on ${record.settlement} but this process now settles on ${s.mode}. ` +
|
|
344
|
+
`Nothing was funded yet (stage ${record.stage}): start a new spend with a new idempotencyKey, or set BLINDMARKET_SETTLEMENT=${record.settlement} to finish this one.`);
|
|
345
|
+
e.code = 'SETTLEMENT_CHANGED';
|
|
346
|
+
throw e;
|
|
347
|
+
}
|
|
348
|
+
const refused = await notPostingChain(s);
|
|
303
349
|
if (refused)
|
|
304
350
|
throw refused;
|
|
305
|
-
|
|
306
|
-
await ensureAllowance(s, record);
|
|
351
|
+
const nonce = isErc20Settlement(s) ? await ensureAllowance(s, record) : undefined;
|
|
307
352
|
const { unsignedTx, chain: builtChain, chainId: builtChainId } = await api('POST', '/api/v1/tasks', {
|
|
308
353
|
taskHash: record.taskHash,
|
|
309
|
-
token: s
|
|
354
|
+
token: isErc20Settlement(s) ? s.token.address : ZERO_TOKEN,
|
|
310
355
|
amount: record.amountWei,
|
|
311
356
|
locationZone: 'global',
|
|
312
357
|
duration: String(record.durationSecs ?? 3600),
|
|
@@ -329,8 +374,8 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
329
374
|
throw e;
|
|
330
375
|
}
|
|
331
376
|
await verifyTarget(s, unsignedTx.to, 'createTask');
|
|
332
|
-
if (s
|
|
333
|
-
const { hash, isUserOp, gas } = await
|
|
377
|
+
if (isErc20Settlement(s)) {
|
|
378
|
+
const { hash, isUserOp, gas } = await sendErc20(s, { to: unsignedTx.to, data: unsignedTx.data }, nonce);
|
|
334
379
|
// Persist BEFORE waiting, same reasoning as the 0G branch below.
|
|
335
380
|
updateSpend(record.idempotencyKey, { stage: 'funded', txHash: hash, isUserOp, gas });
|
|
336
381
|
record.gas = gas;
|
|
@@ -397,7 +442,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
397
442
|
// ── rent_service ──────────────────────────────────────────────────────────
|
|
398
443
|
server.registerTool('rent_service', {
|
|
399
444
|
title: 'Rent an Agent Service',
|
|
400
|
-
description: 'Hire a listed agent service for one call: encrypts your prompt locally (unless privacy=public), funds escrow, and pins the task to the provider agent. Escrow is USDC on
|
|
445
|
+
description: 'Hire a listed agent service for one call: encrypts your prompt locally (unless privacy=public), funds escrow, and pins the task to the provider agent. Escrow is paid on the backend\'s posting chain: USDC on Arc signed by the local wallet (BLINDMARKET_PRIVATE_KEY, which must own BLINDMARKET_API_KEY; gas is also USDC), USDC on Base through the backend relay (no private key needed), or native 0G from the local wallet. wallet_status shows which. TWO-STEP: first call returns a price quote + quoteId; re-call with confirm=true and that quoteId to actually spend. Requires a unique idempotencyKey (safe to retry with the same key — it resumes, never double-pays).',
|
|
401
446
|
inputSchema: {
|
|
402
447
|
serviceId: z.number().int().positive().describe('Service id from browse_services / get_service'),
|
|
403
448
|
prompt: z.string().min(1).max(100_000).describe('What you want the agent to do'),
|
|
@@ -409,11 +454,8 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
409
454
|
},
|
|
410
455
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
411
456
|
}, async ({ serviceId, prompt, idempotencyKey, privacy, confirm, quoteId, waitSeconds }) => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return f.error;
|
|
415
|
-
const { s, payFrom } = f;
|
|
416
|
-
// Resume path — this key already spent (or partially spent).
|
|
457
|
+
// Resume path — this key already spent (or partially spent). Checked
|
|
458
|
+
// before settlement: a funded spend finishes without it.
|
|
417
459
|
const existing = getSpend(idempotencyKey);
|
|
418
460
|
if (existing) {
|
|
419
461
|
if (existing.stage === 'indexed') {
|
|
@@ -429,7 +471,11 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
429
471
|
return fail(err.code ?? 'RESUME_FAILED', err.message);
|
|
430
472
|
}
|
|
431
473
|
}
|
|
432
|
-
const
|
|
474
|
+
const f = await requireFunding();
|
|
475
|
+
if ('error' in f)
|
|
476
|
+
return f.error;
|
|
477
|
+
const { s, payFrom } = f;
|
|
478
|
+
const offPosting = await notPostingChain(s);
|
|
433
479
|
if (offPosting)
|
|
434
480
|
return fail(offPosting.code, offPosting.message);
|
|
435
481
|
const service = await api('GET', `/api/v1/marketplace/services/${serviceId}`);
|
|
@@ -506,7 +552,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
506
552
|
requiredCapabilities: [],
|
|
507
553
|
amountWei: String(service.price_raw),
|
|
508
554
|
settlement: s.mode,
|
|
509
|
-
token: s
|
|
555
|
+
token: isErc20Settlement(s) ? s.token.address : ZERO_TOKEN,
|
|
510
556
|
durationSecs: 3600,
|
|
511
557
|
createdAt: new Date().toISOString(),
|
|
512
558
|
updatedAt: new Date().toISOString(),
|
|
@@ -519,7 +565,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
519
565
|
catch (err) {
|
|
520
566
|
const code = err.code;
|
|
521
567
|
if (code === 'NOT_TASK_AGENT') {
|
|
522
|
-
return fail(code, 'The API key\'s owner wallet does not match the wallet that funded escrow.
|
|
568
|
+
return fail(code, 'The API key\'s owner wallet does not match the wallet that funded escrow. When this process signs (0G, Arc), mint an sk_ key while signed in with the BLINDMARKET_PRIVATE_KEY wallet; on a relay chain (Base) the relay signs from the key\'s own wallet, so this means the key was rotated mid-spend. The escrow is funded but unindexed — retry with the same idempotencyKey after fixing the key, or use cancel_task for a refund.');
|
|
523
569
|
}
|
|
524
570
|
return fail(code ?? 'RENT_FAILED', err.message);
|
|
525
571
|
}
|
|
@@ -527,7 +573,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
527
573
|
// ── post_task ─────────────────────────────────────────────────────────────
|
|
528
574
|
server.registerTool('post_task', {
|
|
529
575
|
title: 'Post a Task to the Open Market',
|
|
530
|
-
description: 'Post a task any matching agent can pick up: encrypts the brief locally and wraps its key to every registered matching executor (or posts it in plaintext with privacy=public), then funds escrow. Escrow is USDC on
|
|
576
|
+
description: 'Post a task any matching agent can pick up: encrypts the brief locally and wraps its key to every registered matching executor (or posts it in plaintext with privacy=public), then funds escrow. Escrow is paid on the backend\'s posting chain: USDC on Arc signed by the local wallet (BLINDMARKET_PRIVATE_KEY, which must own BLINDMARKET_API_KEY; gas is also USDC), USDC on Base through the backend relay (no private key needed), or native 0G from the local wallet. wallet_status shows which. TWO-STEP quote/confirm like rent_service; requires a unique idempotencyKey.',
|
|
531
577
|
inputSchema: {
|
|
532
578
|
instructions: z.string().min(1).max(100_000).describe('The task brief'),
|
|
533
579
|
amount: z.string().regex(/^\d+(\.\d+)?$/).optional().describe('Escrow amount in the settlement token (e.g. "2.5" — USDC on Base, 0G on 0G) — paid to the worker (90%) on verified completion'),
|
|
@@ -541,10 +587,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
541
587
|
},
|
|
542
588
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
543
589
|
}, async ({ instructions, amount, amount0G, idempotencyKey, capabilities, durationSeconds, privacy, confirm, quoteId }) => {
|
|
544
|
-
|
|
545
|
-
if ('error' in f)
|
|
546
|
-
return f.error;
|
|
547
|
-
const { s, payFrom } = f;
|
|
590
|
+
// Checked before settlement: a funded spend finishes without it.
|
|
548
591
|
const existing = getSpend(idempotencyKey);
|
|
549
592
|
if (existing) {
|
|
550
593
|
if (existing.stage === 'indexed') {
|
|
@@ -557,7 +600,11 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
557
600
|
return fail(err.code ?? 'RESUME_FAILED', err.message);
|
|
558
601
|
}
|
|
559
602
|
}
|
|
560
|
-
const
|
|
603
|
+
const f = await requireFunding();
|
|
604
|
+
if ('error' in f)
|
|
605
|
+
return f.error;
|
|
606
|
+
const { s, payFrom } = f;
|
|
607
|
+
const offPosting = await notPostingChain(s);
|
|
561
608
|
if (offPosting)
|
|
562
609
|
return fail(offPosting.code, offPosting.message);
|
|
563
610
|
const isPublic = privacy === 'public';
|
|
@@ -637,7 +684,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
637
684
|
requiredCapabilities: capabilities ?? [],
|
|
638
685
|
amountWei: amountWei.toString(),
|
|
639
686
|
settlement: s.mode,
|
|
640
|
-
token: s
|
|
687
|
+
token: isErc20Settlement(s) ? s.token.address : ZERO_TOKEN,
|
|
641
688
|
durationSecs: durationSeconds ?? 86400,
|
|
642
689
|
createdAt: new Date().toISOString(),
|
|
643
690
|
updatedAt: new Date().toISOString(),
|
|
@@ -649,7 +696,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
649
696
|
catch (err) {
|
|
650
697
|
const code = err.code;
|
|
651
698
|
if (code === 'NOT_TASK_AGENT') {
|
|
652
|
-
return fail(code, 'The API key\'s owner wallet does not match the wallet that funded escrow.
|
|
699
|
+
return fail(code, 'The API key\'s owner wallet does not match the wallet that funded escrow. When this process signs (0G, Arc), mint an sk_ key while signed in with the BLINDMARKET_PRIVATE_KEY wallet; on a relay chain (Base) the relay signs from the key\'s own wallet, so this means the key was rotated mid-spend. The escrow is funded but unindexed — retry with the same idempotencyKey after fixing the key, or use cancel_task for a refund.');
|
|
653
700
|
}
|
|
654
701
|
return fail(code ?? 'POST_FAILED', err.message);
|
|
655
702
|
}
|
|
@@ -711,7 +758,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
711
758
|
e.code = `TASK_NOT_ON_${s.chain.toUpperCase().replace(/-/g, '_')}`;
|
|
712
759
|
return e;
|
|
713
760
|
};
|
|
714
|
-
if (s
|
|
761
|
+
if (!isErc20Settlement(s)) {
|
|
715
762
|
const detail = await api('GET', `/api/v1/tasks/${encodeURIComponent(task)}`);
|
|
716
763
|
if (detail.chain && detail.chain !== s.chain)
|
|
717
764
|
throw onOtherChain(detail.taskId, detail.chain);
|
|
@@ -788,20 +835,22 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
788
835
|
const taskId = record.taskId;
|
|
789
836
|
let { txHash } = record;
|
|
790
837
|
if (record.settlement && record.settlement !== s.mode) {
|
|
791
|
-
const e = new Error(`refund ${record.idempotencyKey} started on ${record.settlement} but
|
|
838
|
+
const e = new Error(`refund ${record.idempotencyKey} started on ${record.settlement} but this process now settles on ${s.mode} — set BLINDMARKET_SETTLEMENT=${record.settlement} to finish it, or finish it from the web app`);
|
|
792
839
|
e.code = 'SETTLEMENT_CHANGED';
|
|
793
840
|
throw e;
|
|
794
841
|
}
|
|
795
842
|
if (record.stage === 'created') {
|
|
796
843
|
const route = record.kind === 'cancel' ? 'cancel' : 'timeout';
|
|
797
|
-
|
|
844
|
+
// Task ids repeat across chains: name this one (the backend knows
|
|
845
|
+
// 'base' and 'arc'; a 0G task is resolved by ownership as before).
|
|
846
|
+
const { unsignedTx } = await api('POST', `/api/v1/tasks/${taskId}/${route}`, isErc20Settlement(s) ? { chain: s.mode } : undefined);
|
|
798
847
|
// The backend resolves the chain that holds the task and builds for it;
|
|
799
848
|
// the tx carries no chainId. If that chain is not the one this mode
|
|
800
849
|
// broadcasts on, stop here — relaying a 0G refund onto another chain
|
|
801
850
|
// lands on an address with no escrow and burns the gas.
|
|
802
851
|
await verifyTarget(s, unsignedTx.to, `${route}Task`);
|
|
803
|
-
if (s
|
|
804
|
-
const { hash, isUserOp, gas } = await
|
|
852
|
+
if (isErc20Settlement(s)) {
|
|
853
|
+
const { hash, isUserOp, gas } = await sendErc20(s, { to: unsignedTx.to, data: unsignedTx.data });
|
|
805
854
|
// Persist BEFORE waiting, same reasoning as the 0G branch below.
|
|
806
855
|
updateSpend(record.idempotencyKey, { stage: 'sent', txHash: hash, isUserOp, gas });
|
|
807
856
|
record.gas = gas;
|
|
@@ -824,7 +873,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
824
873
|
}
|
|
825
874
|
}
|
|
826
875
|
else if (txHash) {
|
|
827
|
-
if (s
|
|
876
|
+
if (isErc20Settlement(s))
|
|
828
877
|
await waitRelayed(s, txHash, record.isUserOp ?? false);
|
|
829
878
|
else
|
|
830
879
|
await walletCtx.provider.waitForTransaction(txHash);
|
|
@@ -834,7 +883,28 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
834
883
|
}
|
|
835
884
|
await waitCancelled(s, taskId);
|
|
836
885
|
updateSpend(record.idempotencyKey, { stage: 'confirmed' });
|
|
837
|
-
return { taskId, txHash: txHash, gas: record.gas };
|
|
886
|
+
return { taskId, txHash: txHash, gas: record.gas, listingClosed: await confirmRefund(s, taskId, txHash, record.isUserOp ?? false) };
|
|
887
|
+
}
|
|
888
|
+
/** Tell the backend the refund landed (POST /tasks/:id/confirm-tx): it
|
|
889
|
+
* checks the receipt and takes the task off the market, which otherwise
|
|
890
|
+
* keeps listing it as open until its deadline. Best effort, since the
|
|
891
|
+
* money has already moved. A relayed user-op hash has no receipt to check. */
|
|
892
|
+
async function confirmRefund(s, taskId, txHash, isUserOp) {
|
|
893
|
+
if (isUserOp)
|
|
894
|
+
return false;
|
|
895
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
896
|
+
try {
|
|
897
|
+
await api('POST', `/api/v1/tasks/${taskId}/confirm-tx`, { txHash, ...(isErc20Settlement(s) ? { chain: s.mode } : {}) });
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
catch (err) {
|
|
901
|
+
// The backend's RPC can lag the receipt this side just saw.
|
|
902
|
+
if (err.code !== 'NOT_CONFIRMED' || attempt === 3)
|
|
903
|
+
return false;
|
|
904
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return false;
|
|
838
908
|
}
|
|
839
909
|
/** Shared resume arm: an idempotencyKey that has already moved. The kind
|
|
840
910
|
* guard matters — the ledger is one namespace shared with rent/post, and
|
|
@@ -1158,6 +1228,16 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1158
1228
|
gas = sent.gas;
|
|
1159
1229
|
await waitRelayed(s, sent.hash, sent.isUserOp);
|
|
1160
1230
|
}
|
|
1231
|
+
else if (s.payment === 'local-erc20') {
|
|
1232
|
+
// Signed locally over this chain's RPC, like the 0G branch below
|
|
1233
|
+
// but with gas estimated: a revert fails here, unpaid.
|
|
1234
|
+
if (tx.from && tx.from.toLowerCase() !== s.payFrom.toLowerCase()) {
|
|
1235
|
+
return fail('WALLET_MISMATCH', `The backend assigned this task to ${tx.from} (the wallet behind BLINDMARKET_API_KEY), but BLINDMARKET_PRIVATE_KEY is ${s.payFrom}. submitEvidence is worker-only — set the private key of ${tx.from}.`);
|
|
1236
|
+
}
|
|
1237
|
+
const sent = await sendErc20(s, { to: tx.to, data: tx.data });
|
|
1238
|
+
submitTxHash = sent.hash;
|
|
1239
|
+
await waitRelayed(s, sent.hash, false);
|
|
1240
|
+
}
|
|
1161
1241
|
else {
|
|
1162
1242
|
// submitEvidence is onlyWorker: the backend built the tx for the
|
|
1163
1243
|
// API key's wallet (tx.from). If BLINDMARKET_PRIVATE_KEY is a
|
|
@@ -1210,6 +1290,215 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1210
1290
|
return fail(err.code ?? 'COMPLETE_FAILED', err.message);
|
|
1211
1291
|
}
|
|
1212
1292
|
});
|
|
1293
|
+
// ── deploy_agent ──────────────────────────────────────────────────────────
|
|
1294
|
+
/** The env var each provider's API key is read from, so the key never passes through the conversation. */
|
|
1295
|
+
const PROVIDER_KEY_ENV = {
|
|
1296
|
+
openai: 'OPENAI_API_KEY',
|
|
1297
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
1298
|
+
groq: 'GROQ_API_KEY',
|
|
1299
|
+
gemini: 'GEMINI_API_KEY',
|
|
1300
|
+
};
|
|
1301
|
+
const coded = (code, message) => Object.assign(new Error(message), { code });
|
|
1302
|
+
/** The local wallet on `chain`, over BLINDMARKET_<CHAIN>_RPC_URL (else a
|
|
1303
|
+
* public RPC for that chain id), checked against the chain id the backend
|
|
1304
|
+
* names, and against `expectChainId` (the fee terms') when given. */
|
|
1305
|
+
async function walletOn(chain, expectChainId) {
|
|
1306
|
+
const res = await fetch(`${cfg.apiBase}/health/bridge`, { signal: AbortSignal.timeout(30_000) });
|
|
1307
|
+
const bridge = await res.json().catch(() => ({}));
|
|
1308
|
+
const entry = (bridge?.data ?? bridge)?.chains?.find((c) => c.chain === chain);
|
|
1309
|
+
const chainId = Number(entry?.chainId);
|
|
1310
|
+
if (!Number.isInteger(chainId) || chainId <= 0)
|
|
1311
|
+
throw coded('SETTLEMENT_UNKNOWN', `The backend lists no chain id for ${chain}.`);
|
|
1312
|
+
if (expectChainId !== undefined && expectChainId !== chainId) {
|
|
1313
|
+
throw coded('SETTLEMENT_UNKNOWN', `The deploy fee is on chain ${expectChainId} but the backend lists ${chain} as chain ${chainId}. Nothing was paid.`);
|
|
1314
|
+
}
|
|
1315
|
+
const envName = rpcEnvName(chain);
|
|
1316
|
+
const rpcUrl = rpcFor(chain, chainId, process.env);
|
|
1317
|
+
if (!rpcUrl)
|
|
1318
|
+
throw coded('RPC_UNKNOWN', `No RPC known for ${chain} (chainId ${chainId}) — set ${envName}.`);
|
|
1319
|
+
const provider = new JsonRpcProvider(rpcUrl, chainId, { staticNetwork: true });
|
|
1320
|
+
const served = Number(BigInt(await provider.send('eth_chainId', [])));
|
|
1321
|
+
if (served !== chainId)
|
|
1322
|
+
throw coded('WRONG_RPC', `${envName} serves chain ${served}, not ${chain} (${chainId}). Nothing was paid.`);
|
|
1323
|
+
return walletCtx.wallet.connect(provider);
|
|
1324
|
+
}
|
|
1325
|
+
server.registerTool('deploy_agent', {
|
|
1326
|
+
title: 'Deploy a Hosted Agent',
|
|
1327
|
+
description: "Deploy a hosted agent that runs on BlindMarket and takes tasks, owned by the API key's wallet. Deploying costs a fee (1 USDC on Arc on production), paid as one USDC transfer on Arc from the local wallet (BLINDMARKET_PRIVATE_KEY), which must be the API key's owner. The model provider's key is read from this server's environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, GROQ_API_KEY or GEMINI_API_KEY; none for 0g-compute), never passed as an argument. TWO-STEP quote/confirm like post_task; requires a unique idempotencyKey, and a retry with the same key never pays twice.",
|
|
1328
|
+
inputSchema: {
|
|
1329
|
+
name: z.string().min(1).max(80).describe('Agent name'),
|
|
1330
|
+
instructions: z.string().min(1).max(100_000).describe("The agent's instructions: what it does and how"),
|
|
1331
|
+
provider: z.enum(['openai', 'anthropic', 'groq', 'gemini', '0g-compute']).describe("LLM provider. '0g-compute' needs no API key: inference is billed to the agent's own wallet"),
|
|
1332
|
+
model: z.string().min(1).describe('Model id, e.g. gpt-4o-mini or claude-sonnet-4-5'),
|
|
1333
|
+
skillSlugs: z.array(z.string()).max(10).optional().describe('Public skills to install at deploy, by slug'),
|
|
1334
|
+
idempotencyKey: z.string().min(8).max(128).describe('Unique key for this deploy — reuse it on retries'),
|
|
1335
|
+
confirm: z.boolean().optional().describe('Set true (with quoteId) to pay the fee and deploy'),
|
|
1336
|
+
quoteId: z.string().optional().describe('From the quote step'),
|
|
1337
|
+
},
|
|
1338
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1339
|
+
}, async ({ name, instructions, provider, model, skillSlugs, idempotencyKey, confirm, quoteId }) => {
|
|
1340
|
+
if (!walletCtx) {
|
|
1341
|
+
return fail('NO_WALLET', 'deploy_agent pays the fee from, and encrypts the agent key to, the local wallet — set BLINDMARKET_PRIVATE_KEY to the key of the wallet that owns BLINDMARKET_API_KEY.');
|
|
1342
|
+
}
|
|
1343
|
+
let apiKey = '';
|
|
1344
|
+
if (provider !== '0g-compute') {
|
|
1345
|
+
const envName = PROVIDER_KEY_ENV[provider];
|
|
1346
|
+
apiKey = process.env[envName] ?? '';
|
|
1347
|
+
if (!apiKey)
|
|
1348
|
+
return fail('PROVIDER_KEY_MISSING', `Set ${envName} in this server's environment: the agent calls ${provider} with it. It is read there so it never passes through the conversation.`);
|
|
1349
|
+
}
|
|
1350
|
+
const body = {
|
|
1351
|
+
name, instructions, provider, model, apiKey,
|
|
1352
|
+
capabilities: [],
|
|
1353
|
+
skillSlugs: skillSlugs ?? [],
|
|
1354
|
+
// The agent's private key is encrypted to the local wallet's key.
|
|
1355
|
+
ownerPublicKey: derivePublicKeyHex(walletCtx.wallet.privateKey),
|
|
1356
|
+
};
|
|
1357
|
+
const summary = (a) => ({ agentId: a.id, name: a.name, walletAddress: a.walletAddress, started: a.started === true });
|
|
1358
|
+
/** POST /agents/deploy, asking again while the backend has not seen the fee confirm. */
|
|
1359
|
+
const deploy = async (feeTxHash) => {
|
|
1360
|
+
for (let attempt = 1;; attempt++) {
|
|
1361
|
+
try {
|
|
1362
|
+
return await api('POST', '/api/v1/agents/deploy', feeTxHash ? { ...body, feeTxHash } : body);
|
|
1363
|
+
}
|
|
1364
|
+
catch (err) {
|
|
1365
|
+
if (err.code !== 'DEPLOY_FEE_NOT_FOUND' || attempt >= 3)
|
|
1366
|
+
throw err;
|
|
1367
|
+
await new Promise((r) => setTimeout(r, Number(process.env.BLINDMARKET_DEPLOY_POLL_MS ?? 5000)));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
/** The deploy's own checks, run before anything is quoted or paid. A
|
|
1372
|
+
* backend without the route (404) is not checked here; the deploy
|
|
1373
|
+
* still checks before it takes the fee. */
|
|
1374
|
+
const validate = async () => {
|
|
1375
|
+
try {
|
|
1376
|
+
await api('POST', '/api/v1/agents/deploy/validate', body);
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
catch (err) {
|
|
1380
|
+
const code = err.code;
|
|
1381
|
+
if (code === undefined && /failed: 404$/.test(err.message))
|
|
1382
|
+
return null;
|
|
1383
|
+
return fail(code ?? 'DEPLOY_INVALID', `${err.message}. Nothing was paid.`);
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
const paidNote = () => {
|
|
1387
|
+
const rec = getSpend(idempotencyKey);
|
|
1388
|
+
return rec?.stage === 'sent' && rec.txHash
|
|
1389
|
+
? ` The fee is paid (${rec.txHash}): retry with the SAME idempotencyKey to deploy without paying again.`
|
|
1390
|
+
: '';
|
|
1391
|
+
};
|
|
1392
|
+
const existing = getSpend(idempotencyKey);
|
|
1393
|
+
if (existing && existing.kind !== 'deploy') {
|
|
1394
|
+
return fail('IDEMPOTENCY_KEY_IN_USE', `idempotencyKey ${idempotencyKey} belongs to a ${existing.kind} spend — use a new key for this deploy.`);
|
|
1395
|
+
}
|
|
1396
|
+
if (existing?.stage === 'confirmed') {
|
|
1397
|
+
return ok({ resumed: true, agentId: existing.agentId, feeTxHash: existing.txHash, hint: 'Already deployed with this idempotencyKey.' });
|
|
1398
|
+
}
|
|
1399
|
+
if (existing?.stage === 'sent' && existing.txHash) {
|
|
1400
|
+
// Paid before: finish the deploy with that payment.
|
|
1401
|
+
try {
|
|
1402
|
+
const agent = await deploy(existing.txHash);
|
|
1403
|
+
updateSpend(idempotencyKey, { stage: 'confirmed', agentId: agent.id });
|
|
1404
|
+
return ok({ resumed: true, ...summary(agent), feeTxHash: existing.txHash });
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
return fail(err.code ?? 'DEPLOY_FAILED', `${err.message}.${paidNote()}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
let terms;
|
|
1411
|
+
try {
|
|
1412
|
+
terms = await api('GET', '/api/v1/agents/deploy-fee');
|
|
1413
|
+
}
|
|
1414
|
+
catch (err) {
|
|
1415
|
+
return fail(err.code ?? 'DEPLOY_FEE_UNKNOWN', err.message);
|
|
1416
|
+
}
|
|
1417
|
+
if (terms.required && terms.method !== 'transfer') {
|
|
1418
|
+
return fail('UNSUPPORTED_FEE_METHOD', 'This backend takes the deploy fee through AgentFactory only, which this server does not pay. Deploy from the web app.');
|
|
1419
|
+
}
|
|
1420
|
+
const fee = terms.required ? terms : null;
|
|
1421
|
+
const feeText = fee ? `${formatUnits(BigInt(fee.amountRaw), fee.decimals).replace(/\.0$/, '')} USDC` : 'none';
|
|
1422
|
+
if (!confirm) {
|
|
1423
|
+
const invalid = await validate();
|
|
1424
|
+
if (invalid)
|
|
1425
|
+
return invalid;
|
|
1426
|
+
let walletBalance;
|
|
1427
|
+
if (fee) {
|
|
1428
|
+
try {
|
|
1429
|
+
const w = await walletOn(fee.chain, fee.chainId);
|
|
1430
|
+
const bal = await new Contract(fee.token, ['function balanceOf(address) view returns (uint256)'], w).balanceOf(w.address);
|
|
1431
|
+
walletBalance = formatUnits(bal, fee.decimals);
|
|
1432
|
+
}
|
|
1433
|
+
catch (err) {
|
|
1434
|
+
if (['RPC_UNKNOWN', 'WRONG_RPC', 'SETTLEMENT_UNKNOWN'].includes(err.code ?? '')) {
|
|
1435
|
+
return fail(err.code, err.message);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const quote = createQuote('deploy', { name, provider, model, fee: feeText });
|
|
1440
|
+
return ok({
|
|
1441
|
+
quote: {
|
|
1442
|
+
agent: { name, provider, model, skills: skillSlugs ?? [] },
|
|
1443
|
+
fee: feeText,
|
|
1444
|
+
chain: fee?.chain,
|
|
1445
|
+
payTo: fee?.recipient,
|
|
1446
|
+
payFrom: walletCtx.wallet.address,
|
|
1447
|
+
walletBalance,
|
|
1448
|
+
quoteId: quote.quoteId,
|
|
1449
|
+
},
|
|
1450
|
+
next: `Re-call deploy_agent with confirm=true, quoteId="${quote.quoteId}", and the SAME idempotencyKey to ${fee ? 'pay the fee and ' : ''}deploy.`,
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
if (!quoteId || !consumeQuote(quoteId, 'deploy')) {
|
|
1454
|
+
return fail('QUOTE_REQUIRED', 'Get a quote first (call without confirm), then re-call with confirm=true and the returned quoteId (quotes are single-use and expire after 10 minutes)');
|
|
1455
|
+
}
|
|
1456
|
+
try {
|
|
1457
|
+
const now = new Date().toISOString();
|
|
1458
|
+
let feeTxHash;
|
|
1459
|
+
if (fee) {
|
|
1460
|
+
// The backend counts a fee only from the API key's owner: check before paying.
|
|
1461
|
+
const { address: owner } = await api('GET', '/api/v1/api-keys/whoami');
|
|
1462
|
+
if (String(owner).toLowerCase() !== walletCtx.wallet.address.toLowerCase()) {
|
|
1463
|
+
return fail('OWNER_MISMATCH', `BLINDMARKET_API_KEY belongs to ${owner} but BLINDMARKET_PRIVATE_KEY is ${walletCtx.wallet.address}. Nothing was paid: the backend counts a deploy fee only from the API key's owner.`);
|
|
1464
|
+
}
|
|
1465
|
+
const invalid = await validate();
|
|
1466
|
+
if (invalid)
|
|
1467
|
+
return invalid;
|
|
1468
|
+
const w = await walletOn(fee.chain, fee.chainId);
|
|
1469
|
+
putSpend({ idempotencyKey, kind: 'deploy', stage: 'created', settlement: fee.chain, token: fee.token, amountWei: fee.amountRaw, createdAt: now, updatedAt: now });
|
|
1470
|
+
const data = new Interface(['function transfer(address to, uint256 amount) returns (bool)'])
|
|
1471
|
+
.encodeFunctionData('transfer', [fee.recipient, BigInt(fee.amountRaw)]);
|
|
1472
|
+
const tx = await w.sendTransaction({ to: fee.token, data });
|
|
1473
|
+
// Recorded the moment it is broadcast: a retry resumes from here and never pays twice.
|
|
1474
|
+
updateSpend(idempotencyKey, { stage: 'sent', txHash: tx.hash });
|
|
1475
|
+
try {
|
|
1476
|
+
await tx.wait();
|
|
1477
|
+
}
|
|
1478
|
+
catch (err) {
|
|
1479
|
+
if (err.code === 'CALL_EXCEPTION') {
|
|
1480
|
+
updateSpend(idempotencyKey, { stage: 'created', txHash: undefined });
|
|
1481
|
+
return fail('FEE_REVERTED', `The fee transfer ${tx.hash} reverted, so nothing was paid. Check the wallet's USDC on ${fee.chain} and retry.`);
|
|
1482
|
+
}
|
|
1483
|
+
// Not confirmed yet: the backend waits for the receipt itself.
|
|
1484
|
+
}
|
|
1485
|
+
feeTxHash = tx.hash;
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
putSpend({ idempotencyKey, kind: 'deploy', stage: 'created', createdAt: now, updatedAt: now });
|
|
1489
|
+
}
|
|
1490
|
+
const agent = await deploy(feeTxHash);
|
|
1491
|
+
updateSpend(idempotencyKey, { stage: 'confirmed', agentId: agent.id });
|
|
1492
|
+
return ok({
|
|
1493
|
+
...summary(agent),
|
|
1494
|
+
feeTxHash,
|
|
1495
|
+
hint: agent.started ? 'The agent is running.' : 'The agent was created but did not start — start it with start_agent.',
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
catch (err) {
|
|
1499
|
+
return fail(err.code ?? 'DEPLOY_FAILED', `${err.message}.${paidNote()}`);
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1213
1502
|
return { settlement };
|
|
1214
1503
|
}
|
|
1215
1504
|
//# sourceMappingURL=rent.js.map
|