@oneshot-agent/sdk 0.60.0 → 0.62.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/dist/index.js CHANGED
@@ -37,6 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
+ const untrusted_content_1 = require("./untrusted-content");
41
+ const tool_timeouts_generated_1 = require("./tool-timeouts.generated");
40
42
  const hunts_1 = require("./hunts");
41
43
  __exportStar(require("./solve"), exports);
42
44
  __exportStar(require("./predict"), exports);
@@ -61,13 +63,9 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
61
63
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
62
64
  __exportStar(require("./errors"), exports);
63
65
  const approvals_1 = require("./approvals");
64
- // `./receipt` is NOT re-exported here, values or types: it statically imports
65
- // `node:crypto`, which would force a hard Node dependency on every consumer
66
- // including browser bundles; and its `SignableReceipt.providerCost` field must
67
- // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
68
- // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
66
+ // Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
69
67
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
70
- const SDK_VERSION = '0.60.0';
68
+ const SDK_VERSION = '0.62.0';
71
69
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
72
70
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
73
71
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -106,12 +104,7 @@ function chainIdFromNetwork(network) {
106
104
  return m ? Number(m[1]) : undefined;
107
105
  }
108
106
  const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
109
- // ETH-currency mode. A payment is an EIP-3009 authorization that the
110
- // facilitator settles AFTER the API has responded, so for seconds after a
111
- // successful call `balanceOf` still shows pre-payment funds. The SDK therefore
112
- // keeps a ledger of signed-but-unsettled authorizations and works from
113
- // `balanceOf − pending`, over-reserving (one early buffered swap) rather than
114
- // under-reserving (a failed settlement).
107
+ // Reserve signed-but-unsettled payments against balanceOf so delayed settlement cannot overspend USDC.
115
108
  /** One balanceOf read per paid call, deduped within roughly one Base block. */
116
109
  const USDC_BALANCE_CACHE_MS = 2000;
117
110
  /** How long a signed authorization stays subtracted from the on-chain balance (settlement lands well within this). */
@@ -139,10 +132,7 @@ __exportStar(require("./types"), exports);
139
132
  * await agent.email({ to: 'user@example.com', subject: 'Hi', body: 'Hello' });
140
133
  * ```
141
134
  */
142
- /**
143
- * Reject a budget config the server would reject, at construction rather than
144
- * on the first paid call — a typo'd cap must not become "no cap".
145
- */
135
+ /** Reject invalid budgets at construction so a typo cannot silently remove the cap. */
146
136
  function validateBudgetConfig(budgets) {
147
137
  if (!budgets)
148
138
  return undefined;
@@ -174,13 +164,13 @@ function validateBudgetConfig(budgets) {
174
164
  }
175
165
  /** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
176
166
  const PAID_ROUTE_OPTION_KEYS = ['timeout', 'totalTimeoutMs', 'onRequestCreated', 'onAccepted', 'signal', 'onStatusUpdate', 'wait', 'waitForPhones', 'phoneTimeoutSec', 'idempotencyKey', 'approvalId', 'maxCost'];
177
- /** ETH mode is unsupported on CDP wallets (#2153): the swap path needs a confirmed receipt CDP never returns. */
167
+ /** ETH mode is unsupported on CDP wallets: the swap path needs a confirmed receipt CDP never returns. */
178
168
  function cdpEthModeError() {
179
169
  return new errors_2.ValidationError("ETH currency mode is not supported with CDP wallets; fund the CDP wallet with USDC or use currency: 'USDC' (or a privateKey / custom walletProvider for ETH mode)", 'currency');
180
170
  }
181
171
  class OneShot {
182
172
  /**
183
- * Async factory — required for CDP wallets (account creation is async).
173
+ * Async factory, required for CDP wallets (account creation is async).
184
174
  * Also works with privateKey and custom walletProvider.
185
175
  *
186
176
  * @example
@@ -196,8 +186,7 @@ class OneShot {
196
186
  if (config.accessToken) {
197
187
  if (config.address)
198
188
  return new OneShot(config);
199
- // The API keys job status and the WebSocket on X-Agent-ID, so the
200
- // session needs its wallet address even though it never signs with it.
189
+ // Job status and WebSocket requests need the wallet address even when the session never signs.
201
190
  const address = await OneShot.resolveTokenAddress(config);
202
191
  return new OneShot({ ...config, address });
203
192
  }
@@ -205,7 +194,7 @@ class OneShot {
205
194
  return new OneShot(config, config.walletProvider);
206
195
  }
207
196
  if (config.cdp) {
208
- // Reject before the CDP SDK is imported or an account is created (#2153).
197
+ // Reject before importing the CDP SDK or creating an account.
209
198
  if ((config.currency ?? 'USDC') === 'ETH')
210
199
  throw cdpEthModeError();
211
200
  const { CdpWalletProvider } = await Promise.resolve().then(() => __importStar(require('./providers/cdp')));
@@ -221,7 +210,7 @@ class OneShot {
221
210
  throw new errors_2.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
222
211
  }
223
212
  /**
224
- * Sync constructor — works with privateKey (backwards compatible).
213
+ * Sync constructor; works with privateKey.
225
214
  * For CDP wallets, use OneShot.create() instead.
226
215
  */
227
216
  constructor(config, walletProvider) {
@@ -260,9 +249,7 @@ class OneShot {
260
249
  this._stagingKey = config.stagingKey;
261
250
  this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
262
251
  if (config.accessToken) {
263
- // Option D: access-token session. Exclusive with every wallet option,
264
- // and budgets are read-only (a delegated credential that could raise its
265
- // own cap would make the cap meaningless) — set them from a wallet.
252
+ // Delegated tokens cannot change their own budgets; caps must be set from a wallet session.
266
253
  if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
267
254
  throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
268
255
  }
@@ -297,9 +284,7 @@ class OneShot {
297
284
  else {
298
285
  throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
299
286
  }
300
- // CDP wallets cannot run ETH mode even though CdpWalletProvider exposes
301
- // sendTransaction: its wait() never observes a receipt, so the swap path
302
- // cannot confirm the ETH→USDC trade (#2153).
287
+ // CDP wait() never confirms a receipt, so ETH mode cannot confirm the swap.
303
288
  if (config.cdp && this._currency === 'ETH')
304
289
  throw cdpEthModeError();
305
290
  if (this._currency === 'ETH' && !this.provider.sendTransaction) {
@@ -342,16 +327,8 @@ class OneShot {
342
327
  this.validate(options.subject, 'subject');
343
328
  }
344
329
  this.validate(options.body, 'body');
345
- // Rotation mode: when the caller passes neither from_domain nor
346
- // from_mailbox, omit from_address entirely so the server picks from
347
- // the agent's domain pool. The chosen address comes back on the
348
- // quote response (`quote.from_address`) and we replay it on /send.
349
- // When the caller pins from_domain (with or without from_mailbox), we
350
- // build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
351
- // alone has no domain to attach it to — `oneshotagent.com` is OneShot's
352
- // own domain and no agent can send from it — so that combination is a
353
- // client-side validation error rather than a guaranteed domain_not_owned
354
- // rejection from the server.
330
+ // Without a pinned sender, the quote locks an address from the agent's domain pool.
331
+ // A mailbox alone has no owned domain to send from.
355
332
  if (options.from_mailbox && !options.from_domain) {
356
333
  throw new errors_2.ValidationError('from_mailbox was pinned without from_domain. Pass from_domain (a domain you own) as well, or omit both to rotate across your warmed domains.', 'from_domain');
357
334
  }
@@ -359,24 +336,14 @@ class OneShot {
359
336
  const fromAddress = useRotation
360
337
  ? undefined
361
338
  : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
362
- // Confirming a new-domain registration (#1070): the caller passes back the
363
- // quote_id of the `confirmation_required` result it was shown. That quote
364
- // is sent as-is — no second quote — so the server charges the stored total
365
- // of the quote the user saw (x402-legacy validates the id and takes the
366
- // amount from the stored quote). A bare `true` is not a confirmation: it
367
- // names no amount, so it falls through to the quote-only path below. An
368
- // expired or already-used quote is refused by the server, and the next
369
- // unconfirmed call produces a fresh quote to show again.
339
+ // Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
370
340
  const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
371
341
  ? options.confirmNewDomain.trim() : undefined;
372
342
  if (confirmedQuote) {
373
343
  this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
374
344
  return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
375
345
  }
376
- // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
377
- // provisions a mailbox on first send — a one-time fee folded into `total_cost`.
378
- // `is_new` (true) means this quote would register a brand-new domain, which
379
- // drives the confirm-before-charge gate below (#1070 round 2).
346
+ // Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
380
347
  const quote = await this.tool('email/quote', {
381
348
  ...(fromAddress ? { from_address: fromAddress } : {}),
382
349
  ...(options.to !== undefined ? { to_address: options.to } : {}),
@@ -384,23 +351,12 @@ class OneShot {
384
351
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
385
352
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
386
353
  body: options.body,
387
- // Forward maxCost so the server-side X-Max-Cost-USDC header is set on
388
- // the quote fetch (executeToolRequest destructures + threads it).
389
354
  maxCost: options.maxCost,
390
355
  });
391
356
  this.log(`Email quote: $${quote.total_cost}`);
392
- // Local fast-fail — matches the per-tool check on commerce/voice/sms/build/
393
- // browser/compute. If the server-side header guard fired we'd never reach
394
- // here; this catches the case where the server's enforcement is bypassed
395
- // (e.g. a future API revision without the header check) or skipped (cap
396
- // un-set so the server returned a 200 with the quote and the caller still
397
- // wants the local guard to apply).
357
+ // Enforce the cap locally even if the server did not check the header.
398
358
  this.assertWithinMaxCost(quote.total_cost, options.maxCost);
399
- // Registering a brand-new domain is charged only after its exact quote has
400
- // been shown and explicitly authorized (#1070 round 2) — this call stops
401
- // here, before /email/send, and charges nothing. Pass confirmNewDomain:
402
- // true (with the same from_domain) to register the domain, pay the quoted
403
- // total, and send the email in one follow-up call.
359
+ // A new domain must be explicitly authorized against its exact quote before any charge or send.
404
360
  if (quote.is_new) {
405
361
  const domainName = quote.domain ?? options.from_domain ?? '';
406
362
  const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
@@ -423,16 +379,11 @@ class OneShot {
423
379
  /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
424
380
  emailSendPayload(options, resolvedFromAddress) {
425
381
  const payload = {
426
- // Server replays the locked address from the quote when from_address
427
- // is absent, but sending it anyway is harmless and forward-compatible
428
- // with future SDK versions that talk directly to /send without quoting.
429
382
  ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
430
383
  ...(options.to !== undefined ? { to_address: options.to } : {}),
431
384
  ...(options.subject !== undefined ? { subject: options.subject } : {}),
432
385
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
433
- // Re-passed on /send so a new-domain mailbox quote still provisions a real
434
- // mailbox even though the domain row doesn't exist yet (server also falls back
435
- // to 'mailbox' when a provisioning fee was paid on the quote).
386
+ // Preserve mailbox provisioning when the new domain does not yet have a stored mode.
436
387
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
437
388
  body: options.body,
438
389
  signal: options.signal,
@@ -446,15 +397,14 @@ class OneShot {
446
397
  payload.attachments = options.attachments;
447
398
  }
448
399
  if (options.idempotencyKey) {
449
- // Only the send call carries the key — the quote call has no side effects.
400
+ // Only sends need idempotency; quoting has no side effects.
450
401
  payload.idempotencyKey = options.idempotencyKey;
451
402
  }
452
403
  if (options.approvalId) {
453
404
  // Releases a send the action policy held (see ApprovalRequiredError).
454
405
  payload.approvalId = options.approvalId;
455
406
  }
456
- // Forward maxCost so the server-side X-Max-Cost-USDC header also guards
457
- // the send, which matters on a confirmed send that skipped the quote.
407
+ // Confirmed sends skip quoting, so the send must also carry the cap.
458
408
  if (options.maxCost !== undefined)
459
409
  payload.maxCost = options.maxCost;
460
410
  return payload;
@@ -515,7 +465,7 @@ class OneShot {
515
465
  const { includePhone, ...payload } = options;
516
466
  return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
517
467
  }
518
- /** At least one identity (#1899): name, email, phone or a profile URL under any of its names. */
468
+ /** At least one identity: name, email, phone or a profile URL under any of its names. */
519
469
  requirePersonIdentity(options) {
520
470
  const hasName = options.name || options.full_name || (options.first_name && options.last_name);
521
471
  if (!hasName && !options.email && !options.phone && !options.profile_url && !options.linkedin_url && !options.social_media_url) {
@@ -562,7 +512,7 @@ class OneShot {
562
512
  return this.tool('resolve/business', { ...options });
563
513
  }
564
514
  /**
565
- * Federal contract opportunities (SAM.gov) by NAICS code — Sources Sought
515
+ * Federal contract opportunities (SAM.gov) by NAICS code - Sources Sought
566
516
  * and Presolicitation notices with the contracting officer's published
567
517
  * contact. Flat price per search; zero notices is a completed result.
568
518
  * `data_as_of` on the result is the snapshot behind the rows (SAM.gov's
@@ -593,7 +543,7 @@ class OneShot {
593
543
  async personDossier(options) {
594
544
  this.requirePersonIdentity(options);
595
545
  const { includePhone, ...payload } = options;
596
- // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run (#1913).
546
+ // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run.
597
547
  return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
598
548
  }
599
549
  async personSocialAccounts(options) {
@@ -629,7 +579,7 @@ class OneShot {
629
579
  if (!response.ok) {
630
580
  throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text(), response.headers.get('retry-after'));
631
581
  }
632
- return response.json();
582
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_list');
633
583
  }
634
584
  async inboxGet(emailId) {
635
585
  this.validate(emailId, 'emailId');
@@ -642,7 +592,7 @@ class OneShot {
642
592
  if (!response.ok) {
643
593
  throw new errors_2.ToolError('Failed to get email', response.status, await response.text(), response.headers.get('retry-after'));
644
594
  }
645
- return response.json();
595
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_get');
646
596
  }
647
597
  async commerceBuy(options) {
648
598
  this.validate(options.product_url, 'product_url');
@@ -754,9 +704,9 @@ class OneShot {
754
704
  if (options.wait !== false && result.request_id) {
755
705
  // Wait for the longest the call may run, plus two minutes to dial and settle.
756
706
  const timeout = options.timeout ?? (options.max_duration_minutes ?? 10) * 60 + 120;
757
- return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate);
707
+ return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate, undefined, 'voice');
758
708
  }
759
- return result;
709
+ return (0, untrusted_content_1.markUntrusted)(result, 'voice');
760
710
  }
761
711
  /**
762
712
  * Send an SMS message
@@ -947,8 +897,6 @@ class OneShot {
947
897
  payload.session_id = options.session_id;
948
898
  if (options.profile_id)
949
899
  payload.profile_id = options.profile_id;
950
- if (options.secrets !== undefined)
951
- throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
952
900
  if (options.max_steps)
953
901
  payload.max_steps = options.max_steps;
954
902
  const { execResp } = await this.runQuoteToPay({
@@ -972,9 +920,9 @@ class OneShot {
972
920
  const result = await execResp.json();
973
921
  this.log(`Browser task initiated: ${result.request_id}`);
974
922
  if (options.wait !== false && result.request_id) {
975
- return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate);
923
+ return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate, undefined, 'browser');
976
924
  }
977
- return result;
925
+ return (0, untrusted_content_1.markUntrusted)(result, 'browser');
978
926
  }
979
927
  /**
980
928
  * Create a persistent browser profile for reusing cookies/localStorage across sessions
@@ -1094,7 +1042,7 @@ class OneShot {
1094
1042
  throw new errors_1.LinkedInConnectRequiredError(body);
1095
1043
  throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
1096
1044
  }
1097
- return response.json();
1045
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), `/v1/tools/linkedin${path}`, method);
1098
1046
  }
1099
1047
  /**
1100
1048
  * Create a hosted LinkedIn login link. Show `url` to the human once; it
@@ -1222,7 +1170,6 @@ class OneShot {
1222
1170
  this.validate(options.identifier, 'identifier');
1223
1171
  const { accountId, identifier, notify, sections, ...rest } = options;
1224
1172
  const body = { ...rest, account_id: accountId, identifier, notify: !!notify, timeout: options.timeout ?? 180 };
1225
- // Left off the wire when empty, so a view without sections is the request it always was.
1226
1173
  if (sections?.length)
1227
1174
  body.sections = sections;
1228
1175
  const result = await this.tool('linkedin/profile-view', body);
@@ -1285,7 +1232,7 @@ class OneShot {
1285
1232
  if (!response.ok) {
1286
1233
  throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
1287
1234
  }
1288
- return response.json();
1235
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_list');
1289
1236
  }
1290
1237
  /**
1291
1238
  * Get this agent a voice number without placing a call. Returns the existing
@@ -1368,7 +1315,7 @@ class OneShot {
1368
1315
  return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1369
1316
  }
1370
1317
  /**
1371
- * Call a declared route of the API by method and path (#1580), the one generic call for a route that has no
1318
+ * Call a declared route of the API by method and path, the one generic call for a route that has no
1372
1319
  * typed method. Follows the Python client's `call_free_get/post/patch/put/delete`:
1373
1320
  *
1374
1321
  * - `path` is the route, with `{name}` (or `:name`) for a path parameter, filled from `params` and URL-encoded
@@ -1448,7 +1395,7 @@ class OneShot {
1448
1395
  if (response.status === 204 || !text)
1449
1396
  return { success: true };
1450
1397
  const json = JSON.parse(text);
1451
- return (json && typeof json === 'object' && 'data' in json ? json.data : json);
1398
+ return (0, untrusted_content_1.markUntrusted)(json && typeof json === 'object' && 'data' in json ? json.data : json, target, method);
1452
1399
  })();
1453
1400
  return await (0, deadline_1.abortable)(request, scope.signal);
1454
1401
  }
@@ -1470,7 +1417,7 @@ class OneShot {
1470
1417
  throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
1471
1418
  }
1472
1419
  const json = await response.json();
1473
- return (json.data ?? json);
1420
+ return (0, untrusted_content_1.markUntrusted)(json.data ?? json, path, method);
1474
1421
  }
1475
1422
  /**
1476
1423
  * Get a specific inbound SMS message
@@ -1492,7 +1439,7 @@ class OneShot {
1492
1439
  if (!response.ok) {
1493
1440
  throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
1494
1441
  }
1495
- return response.json();
1442
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_get');
1496
1443
  }
1497
1444
  /**
1498
1445
  * List notifications for the agent
@@ -1541,8 +1488,8 @@ class OneShot {
1541
1488
  }
1542
1489
  }
1543
1490
  /**
1544
- * The agent's connections (#2403): GitHub and business systems, each with `id`, `system`, `account_label` and
1545
- * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement) takes the `id` of an active
1491
+ * The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
1492
+ * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement, meetings) takes the `id` of an active
1546
1493
  * one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
1547
1494
  * {@link OneShot.route}.
1548
1495
  *
@@ -1570,12 +1517,10 @@ class OneShot {
1570
1517
  return response.json();
1571
1518
  }
1572
1519
  async getBalance(tokenAddress) {
1573
- // Use unified API endpoint for USDC (default) balance
1574
1520
  if (!tokenAddress || tokenAddress === this.usdcAddress) {
1575
1521
  const unified = await this.getUnifiedBalance();
1576
1522
  return unified.on_chain_balance;
1577
1523
  }
1578
- // Custom token: keep existing RPC logic
1579
1524
  const contract = new ethers_1.ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'], this.rpcProvider);
1580
1525
  const [balance, decimals] = await Promise.all([
1581
1526
  contract.balanceOf(this.provider.address),
@@ -1584,11 +1529,16 @@ class OneShot {
1584
1529
  return ethers_1.ethers.formatUnits(balance, decimals);
1585
1530
  }
1586
1531
  /**
1587
- * Create a compute goal — the orchestrator will plan, execute, and iterate autonomously.
1532
+ * Create a compute goal - the orchestrator will plan, execute, and iterate autonomously.
1588
1533
  *
1589
1534
  * Uses the quote-then-pay flow: first call gets a 402 with a budget estimate,
1590
1535
  * second call (with payment) creates the goal.
1591
1536
  *
1537
+ * Name the unit of work, how work starts and what each unit needs in the objective, or pass
1538
+ * `scope`. The quote carries `scope`, `price` (per unit expected and ceiling, overhead, total or
1539
+ * units covered, unpriced, unmeasured) and the `done_check` the goal is held to; `onQuote` reads
1540
+ * them before paying and may throw to stop. A `scope` you pass is priced again in code.
1541
+ *
1592
1542
  * @example
1593
1543
  * ```typescript
1594
1544
  * const goal = await agent.compute({
@@ -1623,7 +1573,11 @@ class OneShot {
1623
1573
  payload.approvals = options.approvals;
1624
1574
  if (options.done_check)
1625
1575
  payload.done_check = options.done_check;
1626
- // Compute returns the goal directly on 202 — no job polling, unlike the
1576
+ if (options.scope)
1577
+ payload.scope = options.scope;
1578
+ if (options.egress_allowlist)
1579
+ payload.egress_allowlist = options.egress_allowlist;
1580
+ // Compute returns the goal directly on 202 - no job polling, unlike the
1627
1581
  // other paid tools.
1628
1582
  const { execResp: createResp } = await this.runQuoteToPay({
1629
1583
  endpoint: '/v1/compute',
@@ -1632,7 +1586,10 @@ class OneShot {
1632
1586
  maxCost: options.maxCost,
1633
1587
  expectMsg: 'Expected 402 for compute quote',
1634
1588
  totalOf: (ctx) => ctx.total_budget,
1635
- onQuote: (ctx) => this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`),
1589
+ onQuote: (ctx) => {
1590
+ this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`);
1591
+ options.onQuote?.(ctx);
1592
+ },
1636
1593
  on400: async (resp) => {
1637
1594
  const errorData = await resp.json();
1638
1595
  if (errorData.error === 'content_blocked') {
@@ -1679,9 +1636,9 @@ class OneShot {
1679
1636
  * Poll a compute goal using its goal_id, not its creation request_id.
1680
1637
  * Returns the complete goal snapshot on completed; failed/cancelled reject
1681
1638
  * with JobError (goal ID, end_reason code, snapshot in partialResult).
1682
- * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1639
+ * Budget exhaustion waits in awaiting_budget for funding; after seven days it ends. After fundComputeGoal revives an ended goal,
1683
1640
  * call waitForComputeGoal again with the same ID to observe the new run.
1684
- * Paused/awaiting_confirmation and recurring goals keep waiting until the
1641
+ * Paused/awaiting_confirmation/awaiting_connection/awaiting_budget/awaiting_tool and recurring goals keep waiting until the
1685
1642
  * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1686
1643
  * Infinity disables the local deadline.
1687
1644
  * onStatusUpdate receives each distinct observed status before settlement.
@@ -1907,6 +1864,54 @@ class OneShot {
1907
1864
  const json = await response.json();
1908
1865
  return json.data;
1909
1866
  }
1867
+ /**
1868
+ * Dispute a goal that ended `impossible_unverified`: the loop claimed it
1869
+ * cannot be done with no proof code could check. The dispute is recorded
1870
+ * on the goal and its receipt; nothing is refunded.
1871
+ *
1872
+ * @example
1873
+ * ```typescript
1874
+ * await agent.disputeComputeGoal(goalId, { reason: 'The data is public.' });
1875
+ * ```
1876
+ */
1877
+ async disputeComputeGoal(goalId, input = {}) {
1878
+ this.validate(goalId, 'goalId');
1879
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1880
+ method: 'POST',
1881
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1882
+ body: JSON.stringify({ dispute: true, ...(input.reason ? { response: input.reason } : {}) }),
1883
+ });
1884
+ if (response.status === 404)
1885
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1886
+ if (!response.ok)
1887
+ throw new errors_2.ToolError('Failed to dispute compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1888
+ const json = await response.json();
1889
+ return json.data;
1890
+ }
1891
+ /**
1892
+ * Continue a goal held in `awaiting_tool` without the tool it requested
1893
+ * (#3173). The goal returns to active and resumes with `message` as what
1894
+ * its owner said, or a default note that it continues without the tool.
1895
+ *
1896
+ * @example
1897
+ * ```typescript
1898
+ * await agent.continueComputeGoalWithoutTool(goalId, { message: 'Use the public filings instead.' });
1899
+ * ```
1900
+ */
1901
+ async continueComputeGoalWithoutTool(goalId, input = {}) {
1902
+ this.validate(goalId, 'goalId');
1903
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1904
+ method: 'POST',
1905
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1906
+ body: JSON.stringify({ continue_without_tool: true, ...(input.message ? { response: input.message } : {}) }),
1907
+ });
1908
+ if (response.status === 404)
1909
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1910
+ if (!response.ok)
1911
+ throw new errors_2.ToolError('Failed to continue compute goal without the tool', response.status, await response.text(), response.headers.get('retry-after'));
1912
+ const json = await response.json();
1913
+ return json.data;
1914
+ }
1910
1915
  /**
1911
1916
  * Pause a recurring compute goal
1912
1917
  *
@@ -1952,7 +1957,7 @@ class OneShot {
1952
1957
  }
1953
1958
  /**
1954
1959
  * Top up budget for a recurring compute goal, or for a goal (one-off or recurring) that
1955
- * ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
1960
+ * is awaiting budget or ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
1956
1961
  *
1957
1962
  * Uses the quote-then-pay flow: the first call gets a 402 with the top-up price,
1958
1963
  * the second call (with payment) credits the budget. The amount IS the price.
@@ -2106,7 +2111,7 @@ class OneShot {
2106
2111
  * List receipts with optional filtering
2107
2112
  *
2108
2113
  * Paginate with `limit`/`offset`, or page through an explicit time window with
2109
- * `since`/`until` (ISO string or Date) — useful for reaching receipts older than
2114
+ * `since`/`until` (ISO string or Date) - useful for reaching receipts older than
2110
2115
  * the server's 100-row cap when tagging value weeks after the originating call.
2111
2116
  *
2112
2117
  * @example
@@ -2144,8 +2149,8 @@ class OneShot {
2144
2149
  * Three ways to address the value:
2145
2150
  * - `rcpt_…` receipt id (from `result.receipt_id` or `receiptsList`), or
2146
2151
  * - the `request_id` returned by the originating tool call (`result.request_id`,
2147
- * resolved server-side via `Receipt.job_id`) — annotate without a list lookup, or
2148
- * - a `goalId` correlation key — attributes a whole *cadence* outcome (the email,
2152
+ * resolved server-side via `Receipt.job_id`) - annotate without a list lookup, or
2153
+ * - a `goalId` correlation key - attributes a whole *cadence* outcome (the email,
2149
2154
  * the follow-ups, the find/verify/enrich calls that share `decisionContext.goalId`)
2150
2155
  * in one call. The value is recorded once in the outcome ledger, so it can't
2151
2156
  * double-count across the cadence's receipts. Read it back with `rocsByGoal()`.
@@ -2159,13 +2164,12 @@ class OneShot {
2159
2164
  * const r = await agent.personEmailStatus({ email: 'x@y.com' });
2160
2165
  * await agent.tagReceiptValue({ requestId: r.request_id }, { type: 'lead', amount: 1 });
2161
2166
  *
2162
- * // By cadence correlation key — one call attributes a closed deal to the whole sequence:
2167
+ * // By cadence correlation key - one call attributes a closed deal to the whole sequence:
2163
2168
  * await agent.tagReceiptValue({ goalId: 'goal_q2_acme' }, { type: 'revenue', amount: 5000, label: 'Closed deal' });
2164
2169
  * ```
2165
2170
  */
2166
2171
  async tagReceiptValue(ref, valueTag) {
2167
2172
  this.validate(valueTag.type, 'valueTag.type');
2168
- // Cadence-level: route to the outcome ledger by correlation key.
2169
2173
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2170
2174
  const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2171
2175
  method: 'POST',
@@ -2192,7 +2196,7 @@ class OneShot {
2192
2196
  }
2193
2197
  }
2194
2198
  /**
2195
- * Per-cadence RoCS rollup — spend (from receipts) vs. value (from outcomes)
2199
+ * Per-cadence RoCS rollup - spend (from receipts) vs. value (from outcomes)
2196
2200
  * grouped by `decisionContext.goalId`. Answers "what did this cadence cost vs.
2197
2201
  * earn" in one query. `value` counts judge-confirmed outcomes; `pending_value`
2198
2202
  * surfaces self-reported outcomes not yet confirmed.
@@ -2273,15 +2277,8 @@ class OneShot {
2273
2277
  jsonHeaders() {
2274
2278
  return { 'Content-Type': 'application/json', ...this.headers() };
2275
2279
  }
2276
- /**
2277
- * Auth headers plus a signed EIP-712 proof (`x-agent-proof`) for the read
2278
- * routes (inbox, sms inbox, notifications, balance). These identify the caller
2279
- * by wallet, and wallet addresses are public, so without a proof anyone could
2280
- * read another agent's data by supplying its address. The proof binds this
2281
- * request to the wallet the SDK controls; the server verifies the signature
2282
- * locally. A fresh nonce per call prevents replay. Signing failure falls back
2283
- * to plain headers, with a one-time warning.
2284
- */
2280
+ /** Signed, single-use proofs bind requests to the wallet; an address alone cannot authenticate reads.
2281
+ * Signing failure falls back to plain headers with a one-time warning. */
2285
2282
  async signedReadHeaders(scope = 'read', domainName = 'OneShot Agent Auth') {
2286
2283
  const base = this.headers();
2287
2284
  // The bearer token already binds the request to the agent (and is
@@ -2293,12 +2290,7 @@ class OneShot {
2293
2290
  const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2294
2291
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2295
2292
  const signature = await this.provider.signTypedData(
2296
- // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
2297
- // its own nonce store — see hunts.ts) use a DIFFERENT EIP-712 domain
2298
- // than api-service's. EIP-712 mixes the domain into the signed hash,
2299
- // so a proof is only valid for the one origin it was signed for and
2300
- // can't be replayed across service boundaries (security finding
2301
- // F-t_d4805d6f-3, round 2).
2293
+ // Separate EIP-712 domains prevent proof replay across services with independent nonce stores.
2302
2294
  { name: domainName, version: '1' }, {
2303
2295
  AgentReadAuth: [
2304
2296
  { name: 'agent', type: 'address' },
@@ -2312,9 +2304,7 @@ class OneShot {
2312
2304
  return { ...base, 'x-agent-proof': proof };
2313
2305
  }
2314
2306
  catch (err) {
2315
- // Once per client, even without debug: when the API enforces proofs,
2316
- // these requests will be rejected, and a silent fallback would leave
2317
- // the caller with a 401 and no idea why.
2307
+ // Warn once even without debug so a failed proof explains the ensuing authentication failure.
2318
2308
  if (!this.readProofWarned) {
2319
2309
  this.readProofWarned = true;
2320
2310
  this.logger(`[OneShot] Warning: could not sign the x-agent-proof read proof, so requests go without it and will be rejected where the API enforces proofs. Pass a signer that supports signTypedData, or use an access token. Cause: ${err}`);
@@ -2325,34 +2315,13 @@ class OneShot {
2325
2315
  return base;
2326
2316
  }
2327
2317
  }
2328
- /** Retry a refused proof once, with a fresh proof, before the API has dispatched the request.
2329
- * Two refusals qualify. A clock-window refusal: the API's `server_time` corrects this client's
2330
- * proof clock (payments and other services keep their own clocks). A read whose nonce the API
2331
- * already spent: the SDK signs every send once and, where the runtime lets it, sends it on a
2332
- * connection of its own (below), so a spent nonce means something between this process and the API
2333
- * delivered the request twice (a retrying proxy, a wrapper re-sending a captured header, or a browser's
2334
- * own retry on a pooled connection); the first copy was answered to nobody.
2335
- * Only a GET or HEAD is sent again for that reason: a read has no side effects, where a write the
2336
- * API already ran could run twice (#2517).
2337
- * Callers supply replayable JSON bodies and retain their original abort/deadline signal.
2338
- */
2318
+ /** Retry a clock-window refusal once using the API's server_time, or a spent nonce on GET/HEAD.
2319
+ * Never retry a spent nonce on a write: the first delivery may already have executed it.
2320
+ * Callers supply replayable JSON bodies and retain their original abort/deadline signal. */
2339
2321
  async fetchApi(url, init) {
2340
2322
  const proof = new Headers(init.headers).get('x-agent-proof');
2341
- // A proof is single-use, so the bytes that carry it must reach the API once. A runtime's
2342
- // fetch pools keep-alive connections and re-sends a request, byte for byte, when the pooled
2343
- // socket it chose closes before the response (Bun 1.3.13; reproduction in docs/status/2517.md):
2344
- // the first copy spends the nonce, the second is rejected as a replay. Each signed send
2345
- // therefore asks for a connection of its own that no later send reuses: `Connection: close` for
2346
- // the server and for a client that honours the header, `keepalive: false` for Bun's pool. In Bun
2347
- // (measured, tests/unit/sdk-proof-single-delivery.test.ts) a socket that dies after the API read
2348
- // the request is then an error to the caller, not a silent duplicate (#2517). This is a request,
2349
- // not a guarantee: a browser's fetch drops `Connection` as a forbidden request header and reads
2350
- // `keepalive` as "may outlive the page", so there the browser's HTTP stack keeps its own pooling and
2351
- // retry rules, and the replayed-read recovery above is the only defence the SDK has; a browser's
2352
- // duplicated write is reported by the API as a replay and is not repeated by the SDK.
2353
- // Both sends below (the first and the one-time refresh) go through `deliver`, so the refresh
2354
- // carries the same boundary and a caller-supplied `Connection` never reaches the wire; the
2355
- // caller's `init` and headers object are left as they were.
2323
+ // Disable connection reuse where supported to prevent transport retries from replaying a single-use proof.
2324
+ // Browsers control their own pooling; only replayed reads can be safely retried there.
2356
2325
  const deliver = (headers) => fetch(url, proof ? { ...init, headers: OneShot.singleDelivery(headers), keepalive: false } : init);
2357
2326
  const response = await deliver(init.headers);
2358
2327
  if (response.status !== 401 || !proof || this._accessToken)
@@ -2393,7 +2362,7 @@ class OneShot {
2393
2362
  init.signal?.throwIfAborted();
2394
2363
  if (!refreshed['x-agent-proof'])
2395
2364
  return response;
2396
- const retryHeaders = new Headers(init.headers); // The first send keeps its own headers; the caller's object is not touched.
2365
+ const retryHeaders = new Headers(init.headers); // Keep the caller's headers intact.
2397
2366
  retryHeaders.set('x-agent-proof', refreshed['x-agent-proof']);
2398
2367
  return deliver(retryHeaders);
2399
2368
  }
@@ -2407,8 +2376,7 @@ class OneShot {
2407
2376
  }
2408
2377
  return { ...Object.fromEntries(Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() !== 'connection')), Connection: 'close' };
2409
2378
  }
2410
- /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2411
- * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2379
+ /** Skip null/undefined query values; callers must explicitly omit other unwanted falsy values. */
2412
2380
  buildQuery(params) {
2413
2381
  const qs = new URLSearchParams();
2414
2382
  for (const [k, v] of Object.entries(params)) {
@@ -2417,20 +2385,8 @@ class OneShot {
2417
2385
  }
2418
2386
  return qs.toString();
2419
2387
  }
2420
- /**
2421
- * Push `config.budgets` to the server once, before the first paid call.
2422
- *
2423
- * Lazy rather than in the constructor because the PUT is signed (async) and
2424
- * the constructor is sync. Idempotent per instance via the cached promise,
2425
- * which is only kept once the sync SUCCEEDS.
2426
- *
2427
- * FAILS CLOSED. If the server can't confirm the budget — network error,
2428
- * 5xx, 429, or a rejected config — this throws BudgetSyncError and the paid
2429
- * call is not made. Proceeding would silently drop the guardrail the
2430
- * developer configured, which is exactly the "empty wallet at 3am" the
2431
- * budget exists to prevent. The marker is cleared so the next paid call
2432
- * retries.
2433
- */
2388
+ /** Sync budgets lazily because signing is async; concurrent calls share the cached promise.
2389
+ * Fail closed until the server confirms the budget, and clear failed syncs so the next call retries. */
2434
2390
  async ensureBudgetsSynced() {
2435
2391
  if (!this._budgets && !this._alertEmail)
2436
2392
  return;
@@ -2470,19 +2426,12 @@ class OneShot {
2470
2426
  await attempt;
2471
2427
  }
2472
2428
  catch (err) {
2473
- // Not synced: clear the marker so the next paid call retries, then
2474
- // refuse this one rather than run it unguarded.
2475
2429
  this._budgetSync = undefined;
2476
2430
  this.log(`${err.message} — paid call refused until the budget is confirmed`);
2477
2431
  throw err;
2478
2432
  }
2479
2433
  }
2480
- /**
2481
- * Local fast-fail on the per-transaction cap, so an oversized call fails
2482
- * without a network round-trip. The server enforces the same cap (and the
2483
- * daily one, which needs the ledger) regardless of which client calls —
2484
- * this is the same SDK-checks/server-enforces split as maxCost.
2485
- */
2434
+ /** Fail oversized calls locally before a round-trip; the server also enforces shared daily spend. */
2486
2435
  assertWithinBudget(total) {
2487
2436
  const cap = this._budgets?.perTransaction;
2488
2437
  if (!cap || cap <= 0)
@@ -2552,7 +2501,7 @@ class OneShot {
2552
2501
  const body = await response.json();
2553
2502
  return body.data;
2554
2503
  }
2555
- /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2504
+ /** List this agent's access tokens (prefix, label, last use, spend today - never the token). */
2556
2505
  async listAccessTokens(opts = {}) {
2557
2506
  this.assertWalletSession('list access tokens');
2558
2507
  const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
@@ -2582,41 +2531,10 @@ class OneShot {
2582
2531
  throw new errors_2.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
2583
2532
  }
2584
2533
  }
2585
- /**
2586
- * Header that asks the API to reject the request when the computed quote
2587
- * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
2588
- * own local `quote.total > maxCost` check after the 402 returns — this is
2589
- * a server-side enforcement layer so non-SDK callers (MCP server, custom
2590
- * integrations) can't ignore the cap.
2591
- */
2592
- /**
2593
- * The amount to sign, in decimal USDC.
2594
- *
2595
- * A 402 advertises its price twice: the x402 v2 `PAYMENT-REQUIRED` header
2596
- * (`accepts[0].amount`, atomic units) and the legacy JSON body
2597
- * (`payment_request.amount`, decimal). The header is authoritative — it is
2598
- * what the server rebuilds its requirement from and what
2599
- * `findMatchingRequirements` compares the signature against.
2600
- *
2601
- * Preferring the body is how quote-based routes (email/send, sms, voice,
2602
- * build, commerce/buy, compute) silently failed: the body carried a
2603
- * hardcoded "0.00", so the SDK signed a zero-cost authorization against a
2604
- * real charge and the server answered with a bodiless 402. It stayed hidden
2605
- * for as long as credits covered those calls, since the credit path returns
2606
- * 202 without any payment handshake.
2607
- *
2608
- * Falls back to the body when the header is missing or unparseable.
2609
- */
2610
- /**
2611
- * Throw the most specific error a failed response supports.
2612
- *
2613
- * A 402 arriving on the PAID retry means the facilitator refused the
2614
- * signature — not the ordinary "here is your quote" 402. The API names the
2615
- * cause in that body (`payment_verification_failed` + reason + expected vs
2616
- * received amount); surface it as a `PaymentError` so callers can branch on
2617
- * `err.reason` instead of string-matching. Anything else keeps the existing
2618
- * `ToolError` shape.
2619
- */
2534
+ /** Ask the API to enforce the caller's cap before payment, in addition to the local quote check. */
2535
+ /** Resolve decimal USDC from the authoritative PAYMENT-REQUIRED header (atomic units);
2536
+ * fall back to the legacy body only when the header is missing or unparseable. */
2537
+ /** A 402 on the paid retry is a payment rejection, not a quote; expose its cause as PaymentError. */
2620
2538
  async failFromResponse(message, response) {
2621
2539
  const text = await response.text();
2622
2540
  const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
@@ -2650,14 +2568,7 @@ class OneShot {
2650
2568
  const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
2651
2569
  return new errors_1.InsufficientCreditsError(body.message ?? 'Credit balance does not cover this call', num(body.required_usdc), num(body.credits_balance), num(body.shortfall_usdc));
2652
2570
  }
2653
- /**
2654
- * Access-token sessions never sign. When a paid call answers 402 the SDK
2655
- * either got the server's `insufficient_credits` verdict (throw it) or a
2656
- * quote (re-POST with `x-quote-id` and no payment: the server executes
2657
- * when the quote's credits cover it, otherwise answers `insufficient_credits`
2658
- * with the balance and shortfall, which failFromResponse turns into the
2659
- * typed error). Server-decided on purpose — the SDK holds no ledger.
2660
- */
2571
+ /** Token sessions never sign: retry the quote against credits and let the server ledger decide sufficiency. */
2661
2572
  async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
2662
2573
  const credits = body ? this.parseInsufficientCredits(body) : undefined;
2663
2574
  if (credits)
@@ -2766,10 +2677,7 @@ class OneShot {
2766
2677
  return bodyAmount ?? '0';
2767
2678
  }
2768
2679
  const fromHeader = ethers_1.ethers.formatUnits(accepted.amount, 6);
2769
- // When the two agree (every fixed-price route), keep the body's string
2770
- // verbatim — downstream consumers such as the ETH auto-swap pass this
2771
- // value along, and there is no reason to reformat it. Only a genuine
2772
- // disagreement flips to the header.
2680
+ // Preserve the body's amount formatting when it agrees with the authoritative header.
2773
2681
  if (bodyAmount != null && parseFloat(bodyAmount) === parseFloat(fromHeader)) {
2774
2682
  return bodyAmount;
2775
2683
  }
@@ -2780,11 +2688,7 @@ class OneShot {
2780
2688
  return undefined;
2781
2689
  return { 'X-Max-Cost-USDC': maxCost.toString() };
2782
2690
  }
2783
- /**
2784
- * Idempotency-Key header, sent on both legs (pre-402 and paid retry) so
2785
- * the server can replay a cached result instead of double-charging and
2786
- * double-executing on a client retry.
2787
- */
2691
+ /** Send the idempotency key on both legs so retries cannot double-charge or repeat the action. */
2788
2692
  idempotencyHeader(idempotencyKey) {
2789
2693
  if (!idempotencyKey)
2790
2694
  return undefined;
@@ -2822,7 +2726,7 @@ class OneShot {
2822
2726
  }
2823
2727
  async executeToolRequest(endpoint, options, quoteId) {
2824
2728
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2825
- const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2729
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2826
2730
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2827
2731
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2828
2732
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2835,7 +2739,7 @@ class OneShot {
2835
2739
  throw new errors_2.OneShotError('Operation cancelled');
2836
2740
  if (key)
2837
2741
  options.onRequestCreated?.({ idempotencyKey: key });
2838
- return await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal);
2742
+ return (0, untrusted_content_1.markUntrusted)(await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal), endpoint);
2839
2743
  }
2840
2744
  catch (error) {
2841
2745
  if (scope.timedOut())
@@ -2858,7 +2762,7 @@ class OneShot {
2858
2762
  Object.assign(extraHeaders, this.approvalHeader(approvalId));
2859
2763
  if (payload.memo !== undefined) {
2860
2764
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
2861
- delete payload.memo; // Drop invalid memo silently
2765
+ delete payload.memo; // Invalid optional metadata must not block the call.
2862
2766
  }
2863
2767
  else if (payload.memo.length > 1000) {
2864
2768
  payload.memo = payload.memo.slice(0, 1000);
@@ -2882,8 +2786,7 @@ class OneShot {
2882
2786
  if (signal?.aborted) {
2883
2787
  throw new errors_2.OneShotError('Operation cancelled');
2884
2788
  }
2885
- // One-time push of config.budgets before the first paid call, so the
2886
- // server-side gate knows about them on this very request.
2789
+ // The server must know the configured budget before the first paid request.
2887
2790
  await this.ensureBudgetsSynced();
2888
2791
  if (signal?.aborted)
2889
2792
  throw new errors_2.OneShotError('Operation cancelled');
@@ -2893,7 +2796,6 @@ class OneShot {
2893
2796
  throw new errors_2.OneShotError('Operation cancelled');
2894
2797
  context.phase = 'submission';
2895
2798
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2896
- // Handle 402 Payment Required
2897
2799
  if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2898
2800
  ({ execResp: response } = await this.runQuoteToPay({
2899
2801
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
@@ -2903,10 +2805,8 @@ class OneShot {
2903
2805
  context.phase = 'submission';
2904
2806
  }
2905
2807
  else if (response.status === 402) {
2906
- // Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
2907
2808
  const paymentRequiredHeader = response.headers.get('payment-required');
2908
2809
  const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
2909
- // Fallback: parse legacy body format for amount display
2910
2810
  const data = await response.json();
2911
2811
  quoteId ?? (quoteId = data.context?.quote_id);
2912
2812
  if (this._accessToken) {
@@ -2946,7 +2846,6 @@ class OneShot {
2946
2846
  await this.failFromResponse('Tool request failed', response);
2947
2847
  }
2948
2848
  const result = await response.json();
2949
- // Handle async jobs
2950
2849
  if ((result.status === 'pending' || result.status === 'processing') && result.request_id) {
2951
2850
  context.requestId = String(result.request_id);
2952
2851
  context.receiptId = typeof result.receipt_id === 'string' ? result.receipt_id : undefined;
@@ -2956,7 +2855,7 @@ class OneShot {
2956
2855
  if (!wait) {
2957
2856
  return { ...result, idempotencyKey };
2958
2857
  }
2959
- const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
2858
+ const completed = await this.pollJob(result.request_id, timeout ?? tool_timeouts_generated_1.TOOL_TIMEOUTS[endpoint], signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
2960
2859
  if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
2961
2860
  return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
2962
2861
  }
@@ -2964,13 +2863,7 @@ class OneShot {
2964
2863
  }
2965
2864
  return (result.data ?? result);
2966
2865
  }
2967
- /**
2968
- * Shared x402 quote-to-pay flow for paid tools. Fetches the 402 quote, runs
2969
- * any per-tool 400 handling, enforces maxCost, signs the payment auth, and
2970
- * POSTs the paid request. Returns the parsed quote context plus the raw paid
2971
- * Response — the caller owns the post-202 handling (poll a job vs. return the
2972
- * body directly), since that differs per tool.
2973
- */
2866
+ /** Shared quote-to-pay flow; callers own post-202 handling because some tools poll and others return directly. */
2974
2867
  async runQuoteToPay(cfg) {
2975
2868
  await this.ensureBudgetsSynced();
2976
2869
  const approval = { ...cfg.extraHeaders, ...this.approvalHeader(cfg.approvalId) };
@@ -2979,8 +2872,6 @@ class OneShot {
2979
2872
  await cfg.on400(quoteResp);
2980
2873
  }
2981
2874
  if (quoteResp.status !== 402) {
2982
- // Routes the budget gate on the quote leg (fixed-price ones) surface a
2983
- // 403 here; failFromResponse maps it to BudgetExceededError.
2984
2875
  await this.failFromResponse(cfg.expectMsg, quoteResp);
2985
2876
  }
2986
2877
  const quoteData = await quoteResp.json();
@@ -3029,14 +2920,20 @@ class OneShot {
3029
2920
  this.validate(requestId, 'requestId');
3030
2921
  return this.pollJob(requestId, options.timeout, options.signal, options.onStatusUpdate, options.waitForPhones ? { waitForPhones: true, phoneTimeoutSec: options.phoneTimeoutSec } : undefined);
3031
2922
  }
3032
- async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts) {
3033
- // HTTP polling is the source of truth; the WebSocket is an accelerator.
3034
- // Both run concurrently from the start and the first terminal outcome wins,
3035
- // so a broken push channel (Redis down, worker unconfigured, proxy that
3036
- // swallows frames) costs nothing beyond the poll cadence. Previously the
3037
- // client waited 60% of its timeout on the socket before polling at all —
3038
- // in prod that was ~72s of dead air on every call while the push never came.
3039
- // An already-aborted signal ends the wait before any proof is signed or sent.
2923
+ unwrapJobResult(job) {
2924
+ const result = job.result ?? job;
2925
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
2926
+ const value = { ...result };
2927
+ for (const field of ['cached', 'fetched_at']) {
2928
+ if (field in job)
2929
+ value[field] = job[field];
2930
+ }
2931
+ return value;
2932
+ }
2933
+ return result;
2934
+ }
2935
+ async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts, tool = '') {
2936
+ // Race HTTP with WebSocket push so a broken push channel cannot stall job completion.
3040
2937
  if (signal?.aborted)
3041
2938
  throw new errors_2.OneShotError('Operation cancelled');
3042
2939
  const inner = new AbortController();
@@ -3088,31 +2985,15 @@ class OneShot {
3088
2985
  signal?.removeEventListener('abort', onOuterAbort);
3089
2986
  }
3090
2987
  this.log(`Job ${requestId} ready after ${Date.now() - startedAt}ms via ${wait.via ?? 'unknown'}`);
3091
- // Optional second phase: keep polling for the async phone-reveal webhook.
3092
- // Only kicks in when the caller explicitly opts in AND the result still
3093
- // has phones_pending=true (set by the worker when the upstream enrichment
3094
- // has a pending async phone callback). This is opt-in so existing callers
3095
- // see no behavior change — the WebSocket/HTTP polls return as soon as the
3096
- // worker sets status=completed, with phones=null.
3097
- //
3098
- // Pass the existing result as the initial fallback: if the polling GETs
3099
- // hit a transient error before any successful refresh, we return what we
3100
- // already have (with phones_pending=true still set so consumers know).
2988
+ // Phone reveals are opt-in; retain the completed snapshot if every refresh fails.
3101
2989
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
3102
2990
  result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3103
2991
  }
3104
2992
  if (phoneOpts?.waitForPhones)
3105
2993
  notify('completed');
3106
- return result;
2994
+ return (0, untrusted_content_1.markUntrusted)(result, tool || wait.resultTool || '');
3107
2995
  }
3108
- /**
3109
- * Detects whether a job result is still waiting for an async phone reveal.
3110
- * The worker sets `result.phones_pending=true` when the upstream enrichment
3111
- * has a pending async webhook and the webhook URL is configured; the webhook
3112
- * handler flips it to false (or removes it) once phones arrive.
3113
- *
3114
- * Accepts worker search rows, profile objects, dossier sections and legacy results.
3115
- */
2996
+ /** Detect pending phone reveals in search rows, profiles, dossier sections and legacy results. */
3116
2997
  _isPhonesPending(result) {
3117
2998
  if (!result || typeof result !== 'object')
3118
2999
  return false;
@@ -3128,19 +3009,10 @@ class OneShot {
3128
3009
  || (Array.isArray(body.results) && body.results.some(pending));
3129
3010
  });
3130
3011
  }
3131
- /**
3132
- * Slow poll loop that waits for async phone-reveal callbacks to arrive via
3133
- * the webhook handler (which UPDATEs jobs.result_data.result.phones). Polls
3134
- * GET /v1/requests/{id} every 5s — the upstream provider can take several
3135
- * minutes to deliver, so a tight loop just wastes API calls. Returns the
3136
- * latest snapshot whether or not phones arrived (consumer can re-check
3137
- * `phones_pending`).
3138
- */
3012
+ /** Poll phone reveals every 5s to avoid wasting calls during slow callbacks; return the latest snapshot on timeout. */
3139
3013
  async _pollForPhones(requestId, timeoutSec, signal, initialResult) {
3140
3014
  const deadline = Date.now() + timeoutSec * 1000;
3141
3015
  const interval = 5000;
3142
- // Seed with the snapshot the caller already has so a transient first-poll
3143
- // failure can still return the sync data (with phones_pending=true).
3144
3016
  let lastResult = initialResult;
3145
3017
  while (Date.now() < deadline) {
3146
3018
  if (signal?.aborted)
@@ -3151,14 +3023,13 @@ class OneShot {
3151
3023
  signal,
3152
3024
  });
3153
3025
  if (!resp.ok) {
3154
- // Soft-fail on transient errors during the phone wait — return
3155
- // whatever we last had so consumers don't lose the sync result.
3026
+ // Preserve the last successful snapshot across transient phone-wait errors.
3156
3027
  if (lastResult !== undefined)
3157
3028
  return lastResult;
3158
3029
  throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
3159
3030
  }
3160
3031
  const job = await resp.json();
3161
- lastResult = (job.result ?? job);
3032
+ lastResult = this.unwrapJobResult(job);
3162
3033
  if (!this._isPhonesPending(lastResult)) {
3163
3034
  return lastResult;
3164
3035
  }
@@ -3174,38 +3045,17 @@ class OneShot {
3174
3045
  }
3175
3046
  await this.sleep(interval, signal);
3176
3047
  }
3177
- // Timeout — return the last snapshot. phones_pending will still be true
3178
- // so the consumer knows phones never arrived.
3179
3048
  return lastResult;
3180
3049
  }
3181
- /**
3182
- * WebSocket branch of a job wait. Resolves on a `completed` push for this
3183
- * request, rejects with `JobError` on `failed` and with `OneShotError` on
3184
- * abort. Every other failure (no WebSocket global, handshake refused, socket
3185
- * closed early) rejects with a plain `Error`, which `pollJob` treats as
3186
- * "no push available" rather than as an outcome. There is no timeout here:
3187
- * the HTTP branch owns the deadline and aborts this one when it settles.
3188
- */
3050
+ /** WebSocket transport failures leave HTTP polling in charge; job outcomes and cancellation settle the wait.
3051
+ * The HTTP branch owns the deadline and aborts this branch when it settles. */
3189
3052
  async waitViaWebSocket(requestId, signal, emit, wait) {
3190
- // Access-token sessions have no wallet signer to produce a signed proof,
3191
- // and the standard WebSocket constructor (browser and Node globals alike)
3192
- // cannot set a custom `Authorization` header on the handshake request —
3193
- // so there is no way to authenticate this upgrade without putting the
3194
- // reusable bearer token in the URL, which issue #971 round 1 (finding
3195
- // F-t_4d087f2a-5) forbids: a bearer secret must never ride in a URL that
3196
- // proxies, access logs, and telemetry can capture. These sessions skip
3197
- // the WebSocket accelerator entirely and rely on HTTP polling, which
3198
- // authenticates via the Authorization header on every request.
3053
+ // WebSocket cannot carry an Authorization header; keep reusable bearer tokens out of logged URLs.
3054
+ // Token sessions use authenticated HTTP polling instead.
3199
3055
  if (this.isAccessTokenSession) {
3200
3056
  throw new Error('WebSocket push is not available for access-token sessions; falling back to HTTP polling');
3201
3057
  }
3202
- // Build the signed, single-use read proof BEFORE opening the socket: the
3203
- // server authenticates the upgrade itself (not just the post-upgrade
3204
- // connection) the same as agentProofAuth does for every other free-read
3205
- // route (issue #971) — an unsigned ?wallet= alone is no longer
3206
- // sufficient. Unlike a bearer token, a proof is short-lived and single-use
3207
- // (5-minute TTL, Redis-enforced nonce), so passing it as a URL query
3208
- // param is an acceptable channel for it.
3058
+ // Authenticate the upgrade with a short-lived, single-use proof; a wallet address alone proves nothing.
3209
3059
  const authHeaders = await this.signedReadHeaders();
3210
3060
  const authParams = new URLSearchParams();
3211
3061
  if (authHeaders['x-agent-proof'])
@@ -3267,7 +3117,8 @@ class OneShot {
3267
3117
  settle(() => {
3268
3118
  cleanup();
3269
3119
  wait.via = 'ws';
3270
- const result = (msg.result ?? msg);
3120
+ wait.resultTool = typeof msg.result_tool === 'string' ? msg.result_tool : undefined;
3121
+ const result = this.unwrapJobResult(msg);
3271
3122
  if (msg.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3272
3123
  result.request_id = msg.request_id;
3273
3124
  }
@@ -3293,7 +3144,6 @@ class OneShot {
3293
3144
  });
3294
3145
  };
3295
3146
  ws.onclose = () => {
3296
- // Any close before we got a result means no push is coming.
3297
3147
  settle(() => {
3298
3148
  cleanup();
3299
3149
  reject(new Error('WebSocket closed before result'));
@@ -3341,7 +3191,7 @@ class OneShot {
3341
3191
  });
3342
3192
  if (!resp.ok) {
3343
3193
  const body = await resp.text();
3344
- // 5xx / 429 from the poll endpoint are transient — keep polling.
3194
+ // 5xx / 429 from the poll endpoint are transient - keep polling.
3345
3195
  // Any other non-2xx (401/403/404) is a real answer about this job.
3346
3196
  if (resp.status >= 500 || resp.status === 429) {
3347
3197
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
@@ -3352,10 +3202,11 @@ class OneShot {
3352
3202
  emit?.(String(job.status));
3353
3203
  if (job.status === 'completed') {
3354
3204
  this.log('Job completed');
3355
- if (wait)
3205
+ if (wait) {
3356
3206
  wait.via = 'http';
3357
- const result = (job.result ?? job);
3358
- // Propagate request_id into the result so callers always have it
3207
+ wait.resultTool = typeof job.result_tool === 'string' ? job.result_tool : undefined;
3208
+ }
3209
+ const result = this.unwrapJobResult(job);
3359
3210
  if (job.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3360
3211
  result.request_id = job.request_id;
3361
3212
  }
@@ -3410,9 +3261,6 @@ class OneShot {
3410
3261
  }
3411
3262
  async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3412
3263
  let resp = await this.sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders);
3413
- // The API refuses a request whose wallet proof it rejected, saying why (#1437).
3414
- // A stale clock window or a proof sent twice is cured by signing a new proof:
3415
- // send the leg again once. Nothing is paid in answer to this refusal.
3416
3264
  for (let attempt = 0; resp.status === 401; attempt++) {
3417
3265
  const refusal = await this.readInvalidProof(resp);
3418
3266
  if (!refusal)
@@ -3441,25 +3289,13 @@ class OneShot {
3441
3289
  }
3442
3290
  }
3443
3291
  async sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3444
- // A leg with no payment attached proves the wallet with a signed agent
3445
- // proof: the API spends an agent's credits, and shows them on a quote, only
3446
- // for a caller that has proven it holds the wallet (a bare X-Agent-ID is a
3447
- // public address). A leg that carries a payment signature needs none. The
3448
- // endpoints with durable admission read a 'submit' proof; a caller that
3449
- // asked for a proof explicitly keeps the scope it always had.
3450
- //
3451
- // One payment leg does need a proof: when credits cover the whole cost the
3452
- // "payment" is the zero-cost authorization, an unsigned stub (signature
3453
- // '0x'). It proves nothing, and the API refuses to spend credits on it
3454
- // alone (401 proof_required).
3292
+ // Credit spending requires wallet proof unless a real payment signature already proves ownership.
3293
+ // Zero-cost authorizations are unsigned stubs, so those still need a proof.
3455
3294
  const unsignedStub = payment?.payload?.signature === '0x';
3456
- // Identity Verification (#2405) answers a retry from the start its key already made, before payment, and
3457
- // only for a caller that proves the wallet: its payment leg carries the agent proof too, however the key was
3458
- // given (an option, or the idempotency_key body field the generated MCP tool sends). The API refuses a
3459
- // payment signature alone there with agent_proof_required. Same list as oneshot-python.
3295
+ // Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
3460
3296
  const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
3461
3297
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3462
- const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
3298
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict)$/.test(endpoint)
3463
3299
  ? 'submit'
3464
3300
  : askedForProof ? 'read' : 'write';
3465
3301
  const headers = {
@@ -3503,14 +3339,8 @@ class OneShot {
3503
3339
  throw new errors_2.OneShotError('Operation cancelled before payment');
3504
3340
  }
3505
3341
  }
3506
- /**
3507
- * ETH mode only. Make sure the wallet's *effective* USDC (on-chain balance
3508
- * minus payments signed but not yet settled) covers this charge, swapping
3509
- * ETH→USDC for a buffer of `swapBufferMultiplier` payments when it does not,
3510
- * then reserve the charge. Returns the reservation so the caller can release
3511
- * it if the payment never settles (request failed). Runs under a per-instance
3512
- * lock so concurrent calls never double-swap or race the wallet nonce.
3513
- */
3342
+ /** Reserve effective USDC (on-chain minus unsettled payments), swapping a buffer if needed.
3343
+ * Serialize reservations and swaps to avoid overspending and nonce races; release failed payments. */
3514
3344
  async ensureUsdcBalance(paymentInfo) {
3515
3345
  if (this._currency !== 'ETH')
3516
3346
  return undefined;
@@ -3591,18 +3421,10 @@ class OneShot {
3591
3421
  if (reservation)
3592
3422
  this._usdcPending.delete(reservation.id);
3593
3423
  }
3594
- /**
3595
- * How much USDC to buy: top up to `charge × swapBufferMultiplier`, counting
3596
- * whatever effective balance is already there. When the wallet provider
3597
- * exposes `getBalance` and its ETH cannot cover the buffered quote, fall
3598
- * back to the bare shortfall so an ETH-poor wallet can still make the one
3599
- * payment in front of it (a send-only provider always gets the buffer).
3600
- */
3424
+ /** Buy a buffer, or only the shortfall when getBalance shows insufficient ETH for the buffer. */
3601
3425
  async sizeSwap(charge, effective, chainId) {
3602
3426
  const shortfall = charge - effective;
3603
- // Integer math on a 10^6 scale (multiplier ≤ 1000, so ≤ 10^9 — always a
3604
- // finite, exact integer): keeps the multiplier's precision, no float→BigInt
3605
- // on an unbounded value.
3427
+ // Bounded fixed-point math preserves precision without converting an unbounded float to BigInt.
3606
3428
  const mScaled = BigInt(Math.round(this._swapBufferMultiplier * SWAP_MULTIPLIER_SCALE));
3607
3429
  const target = (charge * mScaled + BigInt(SWAP_MULTIPLIER_SCALE) - 1n) / BigInt(SWAP_MULTIPLIER_SCALE);
3608
3430
  let amount = target > effective ? target - effective : shortfall;
@@ -3616,7 +3438,7 @@ class OneShot {
3616
3438
  }
3617
3439
  }
3618
3440
  catch {
3619
- // Quoting failed — let executeSwap quote again and report properly.
3441
+ // Quoting failed - let executeSwap quote again and report properly.
3620
3442
  }
3621
3443
  }
3622
3444
  return amount;
@@ -3627,7 +3449,7 @@ class OneShot {
3627
3449
  const quote = await getSwapQuote(this.rpcProvider, usdcAmount, chainId, this._slippage);
3628
3450
  return quote?.amountInMax;
3629
3451
  }
3630
- /** Perform the on-chain swap. */
3452
+ /** Swap on the configured chain using the wallet provider. */
3631
3453
  async executeUsdcSwap(usdcAmount, chainId) {
3632
3454
  const { executeSwap } = await Promise.resolve().then(() => __importStar(require('./swap')));
3633
3455
  return executeSwap(this.provider, this.rpcProvider, usdcAmount, chainId, this._slippage);
@@ -3644,7 +3466,7 @@ class OneShot {
3644
3466
  }
3645
3467
  catch (err) {
3646
3468
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3647
- if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3469
+ if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3648
3470
  this.releaseUsdcReservation(signed.reservation);
3649
3471
  throw err;
3650
3472
  }
@@ -3660,7 +3482,6 @@ class OneShot {
3660
3482
  ? Buffer.from(header, 'base64').toString()
3661
3483
  : atob(header);
3662
3484
  const parsed = JSON.parse(decoded);
3663
- // x402 v2: { x402Version: 2, accepts: [...], resource: {...}, extensions: {...} }
3664
3485
  if (parsed.accepts?.length > 0) {
3665
3486
  return {
3666
3487
  accepted: parsed.accepts[0],
@@ -3673,7 +3494,6 @@ class OneShot {
3673
3494
  this.log('Failed to parse PAYMENT-REQUIRED header, using defaults');
3674
3495
  }
3675
3496
  }
3676
- // Fallback: construct from known production values
3677
3497
  return {
3678
3498
  accepted: {
3679
3499
  scheme: 'exact',
@@ -3696,14 +3516,13 @@ class OneShot {
3696
3516
  if (header) {
3697
3517
  return this.parsePaymentRequired(header);
3698
3518
  }
3699
- // Probe: send quote ID without payment to get x402 middleware's 402
3700
3519
  // Carries X-Approval-Id: a policy-held call's probe must reach the 402,
3701
3520
  // not be held again.
3702
3521
  const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
3703
3522
  return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
3704
3523
  }
3705
3524
  async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {
3706
- // Credits may cover the full cost — send a zero-cost authorization so the server
3525
+ // Credits may cover the full cost - send a zero-cost authorization so the server
3707
3526
  // always receives a payment-signature header (avoids 402 from x402 SDK).
3708
3527
  if (parseFloat(paymentInfo.amount) === 0) {
3709
3528
  this.log('Credits cover full cost — sending zero-cost authorization');
@@ -3725,12 +3544,9 @@ class OneShot {
3725
3544
  },
3726
3545
  } };
3727
3546
  }
3728
- // Belt and braces: every paid path branches to retryAsCreditsSession
3729
- // before reaching here in access-token mode.
3730
3547
  if (this._accessToken) {
3731
3548
  throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
3732
3549
  }
3733
- // If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
3734
3550
  const reservation = await this.ensureUsdcBalance(paymentInfo);
3735
3551
  const now = Math.floor(Date.now() / 1000);
3736
3552
  const nonce = ethers_1.ethers.randomBytes(32);
@@ -3738,14 +3554,10 @@ class OneShot {
3738
3554
  const validAfter = now - 300; // Buffer for clock skew
3739
3555
  const validBefore = now + 3600;
3740
3556
  const nonceHex = ethers_1.ethers.hexlify(nonce);
3741
- // Use the EIP-712 domain from the server's payment requirements — name,
3742
- // version AND chain. The chain used to be the mainnet constant, which made
3743
- // every signature invalid against Base Sepolia (the domain separator
3744
- // includes chainId), so the SDK could never pay on staging.
3557
+ // Use the server's full EIP-712 domain, including chainId, so the signature matches its network.
3745
3558
  const domainName = accepted.extra?.name || 'USD Coin';
3746
3559
  const domainVersion = accepted.extra?.version || '2';
3747
3560
  const chainId = chainIdFromNetwork(accepted.network ?? paymentInfo.network) ?? CHAIN_ID;
3748
- // Sign EIP-3009 TransferWithAuthorization
3749
3561
  let signature;
3750
3562
  try {
3751
3563
  signature = await this.provider.signTypedData({
@@ -3775,7 +3587,6 @@ class OneShot {
3775
3587
  this.releaseUsdcReservation(reservation);
3776
3588
  throw err;
3777
3589
  }
3778
- // Return x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
3779
3590
  return { reservation, auth: {
3780
3591
  x402Version: 2,
3781
3592
  ...(resource ? { resource } : {}),