@oneshot-agent/sdk 0.32.1 → 0.34.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
@@ -36,11 +36,12 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
39
+ exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
40
  const physical_mail_1 = require("./physical-mail");
41
41
  __exportStar(require("./physical-mail"), exports);
42
42
  const deadline_1 = require("./deadline");
43
43
  const errors_1 = require("./errors");
44
+ const read_only_1 = require("./providers/read-only");
44
45
  const ethers_1 = require("ethers");
45
46
  const ethers_2 = require("./providers/ethers");
46
47
  const errors_2 = require("./errors");
@@ -48,12 +49,14 @@ var ethers_3 = require("./providers/ethers");
48
49
  Object.defineProperty(exports, "EthersWalletProvider", { enumerable: true, get: function () { return ethers_3.EthersWalletProvider; } });
49
50
  var cdp_1 = require("./providers/cdp");
50
51
  Object.defineProperty(exports, "CdpWalletProvider", { enumerable: true, get: function () { return cdp_1.CdpWalletProvider; } });
52
+ var read_only_2 = require("./providers/read-only");
53
+ Object.defineProperty(exports, "ReadOnlyWalletProvider", { enumerable: true, get: function () { return read_only_2.ReadOnlyWalletProvider; } });
51
54
  var swap_1 = require("./swap");
52
55
  Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
53
56
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
54
57
  __exportStar(require("./errors"), exports);
55
58
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
56
- const SDK_VERSION = '0.32.1';
59
+ const SDK_VERSION = '0.34.0';
57
60
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
58
61
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
59
62
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -160,6 +163,14 @@ class OneShot {
160
163
  * ```
161
164
  */
162
165
  static async create(config) {
166
+ if (config.accessToken) {
167
+ if (config.address)
168
+ return new OneShot(config);
169
+ // The API keys job status and the WebSocket on X-Agent-ID, so the
170
+ // session needs its wallet address even though it never signs with it.
171
+ const address = await OneShot.resolveTokenAddress(config);
172
+ return new OneShot({ ...config, address });
173
+ }
163
174
  if (config.walletProvider) {
164
175
  return new OneShot(config, config.walletProvider);
165
176
  }
@@ -204,8 +215,39 @@ class OneShot {
204
215
  this._swapBufferMultiplier = validateSwapBufferMultiplier(config.swapBufferMultiplier);
205
216
  this._budgets = validateBudgetConfig(config.budgets);
206
217
  this._alertEmail = config.alerts?.email;
218
+ this._defaultHeaders = { ...(config.defaultHeaders ?? {}) };
219
+ this._stagingKey = config.stagingKey;
207
220
  this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
208
- if (walletProvider) {
221
+ if (config.accessToken) {
222
+ // Option D: access-token session. Exclusive with every wallet option,
223
+ // and budgets are read-only (a delegated credential that could raise its
224
+ // own cap would make the cap meaningless) — set them from a wallet.
225
+ if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
226
+ throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
227
+ }
228
+ if (config.privateKey || config.cdp || config.walletProvider || walletProvider) {
229
+ throw new errors_2.ValidationError('accessToken cannot be combined with privateKey, cdp, or walletProvider', 'accessToken');
230
+ }
231
+ if (config.budgets || config.alerts) {
232
+ throw new errors_2.ValidationError('budgets and alerts are read-only for access-token sessions; set them from a wallet session or the API', 'budgets');
233
+ }
234
+ if (this._currency === 'ETH') {
235
+ throw new errors_2.ValidationError('ETH currency mode is not available to access-token sessions (they pay from credits)', 'currency');
236
+ }
237
+ if (!config.address) {
238
+ throw new errors_2.ValidationError('address is required with accessToken in the constructor; use OneShot.create() to resolve it', 'address');
239
+ }
240
+ let address;
241
+ try {
242
+ address = ethers_1.ethers.getAddress(config.address);
243
+ }
244
+ catch {
245
+ throw new errors_2.ValidationError('address must be a valid Ethereum address', 'address');
246
+ }
247
+ this._accessToken = config.accessToken;
248
+ this.provider = new read_only_1.ReadOnlyWalletProvider(address);
249
+ }
250
+ else if (walletProvider) {
209
251
  this.provider = walletProvider;
210
252
  }
211
253
  else if (config.privateKey) {
@@ -264,12 +306,19 @@ class OneShot {
264
306
  // from_mailbox, omit from_address entirely so the server picks from
265
307
  // the agent's domain pool. The chosen address comes back on the
266
308
  // quote response (`quote.from_address`) and we replay it on /send.
267
- // When the caller pins either knob, we keep legacy behavior: build
268
- // `${mailbox ?? 'agent'}@${domain ?? 'oneshotagent.com'}`.
309
+ // When the caller pins from_domain (with or without from_mailbox), we
310
+ // build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
311
+ // alone has no domain to attach it to — `oneshotagent.com` is OneShot's
312
+ // own domain and no agent can send from it — so that combination is a
313
+ // client-side validation error rather than a guaranteed domain_not_owned
314
+ // rejection from the server.
315
+ if (options.from_mailbox && !options.from_domain) {
316
+ 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');
317
+ }
269
318
  const useRotation = !options.from_domain && !options.from_mailbox;
270
319
  const fromAddress = useRotation
271
320
  ? undefined
272
- : `${options.from_mailbox ?? 'agent'}@${options.from_domain ?? 'oneshotagent.com'}`;
321
+ : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
273
322
  // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
274
323
  // provisions a mailbox on first send — a one-time fee folded into `total_cost`.
275
324
  const quote = await this.tool('email/quote', {
@@ -748,8 +797,8 @@ class OneShot {
748
797
  if (options.task.length < 10) {
749
798
  throw new errors_2.ValidationError('Task must be at least 10 characters', 'task');
750
799
  }
751
- if (options.max_steps !== undefined && (options.max_steps < 1 || options.max_steps > 100)) {
752
- throw new errors_2.ValidationError('max_steps must be between 1 and 100', 'max_steps');
800
+ if (options.max_steps !== undefined && (!Number.isInteger(options.max_steps) || options.max_steps < 25 || options.max_steps > 100)) {
801
+ throw new errors_2.ValidationError('max_steps must be between 25 and 100', 'max_steps');
753
802
  }
754
803
  const payload = {
755
804
  task: options.task,
@@ -764,8 +813,8 @@ class OneShot {
764
813
  payload.session_id = options.session_id;
765
814
  if (options.profile_id)
766
815
  payload.profile_id = options.profile_id;
767
- if (options.secrets)
768
- payload.secrets = options.secrets;
816
+ if (options.secrets !== undefined)
817
+ throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
769
818
  if (options.max_steps)
770
819
  payload.max_steps = options.max_steps;
771
820
  const { execResp } = await this.runQuoteToPay({
@@ -802,18 +851,42 @@ class OneShot {
802
851
  * console.log(profile.id); // Use this in browser({ profile_id: ... })
803
852
  * ```
804
853
  */
805
- async createBrowserProfile(name) {
854
+ async createBrowserProfile(name, options = {}) {
806
855
  this.validate(name, 'name');
856
+ if (options.cookies !== undefined && options.storage_state !== undefined)
857
+ throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
807
858
  const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
808
859
  method: 'POST',
809
860
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
810
- body: JSON.stringify({ name }),
861
+ body: JSON.stringify({ name, ...options }),
862
+ signal: AbortSignal.timeout(150000),
811
863
  });
812
864
  if (!response.ok) {
813
865
  throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
814
866
  }
815
867
  return response.json();
816
868
  }
869
+ /** Open a profile login session; wait for idle before entering credentials in live_url. */
870
+ async startBrowserProfileSetup(profileId, startUrl) {
871
+ return this.browserProfileSetup(profileId, 'start', { start_url: startUrl });
872
+ }
873
+ async getBrowserProfileSetup(profileId) {
874
+ return this.browserProfileSetup(profileId, 'status');
875
+ }
876
+ /** Call after login and 2FA. Saves state and inspects cookies before site navigation. */
877
+ async finishBrowserProfileSetup(profileId) {
878
+ return this.browserProfileSetup(profileId, 'finish');
879
+ }
880
+ async browserProfileSetup(profileId, action, body = {}) {
881
+ this.validate(profileId, 'profileId');
882
+ const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
883
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
884
+ body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
885
+ });
886
+ if (!response.ok)
887
+ throw new errors_2.ToolError('Browser profile setup failed', response.status, '');
888
+ return response.json();
889
+ }
817
890
  /**
818
891
  * List all browser profiles
819
892
  *
@@ -1246,6 +1319,13 @@ class OneShot {
1246
1319
  const quoteData = await quoteResp.json();
1247
1320
  this.log(`Compute fund quote: $${quoteData.payment_request.amount} to top up ${goalId}`);
1248
1321
  this.assertWithinBudget(quoteData.payment_request.amount);
1322
+ if (this._accessToken) {
1323
+ const fundResp = await this.retryAsCreditsSession(quoteData, path, payload, quoteData.context.quote_id);
1324
+ if (!fundResp.ok)
1325
+ await this.failFromResponse('Failed to fund compute goal', fundResp);
1326
+ const json = await fundResp.json();
1327
+ return json.data;
1328
+ }
1249
1329
  const paymentInfo = {
1250
1330
  protocol: 'x402',
1251
1331
  network: `eip155:${quoteData.payment_request.chain_id}`,
@@ -1438,10 +1518,47 @@ class OneShot {
1438
1518
  }
1439
1519
  headers() {
1440
1520
  return {
1521
+ ...this._defaultHeaders,
1522
+ ...(this._stagingKey ? { 'X-Staging-Key': this._stagingKey } : {}),
1441
1523
  'X-Agent-ID': this.provider.address,
1442
- 'X-OneShot-SDK-Version': SDK_VERSION
1524
+ 'X-OneShot-SDK-Version': SDK_VERSION,
1525
+ ...(this._accessToken ? { Authorization: `Bearer ${this._accessToken}` } : {}),
1443
1526
  };
1444
1527
  }
1528
+ /** Submit a wallet-proven application. Approval is performed by the staging operator. */
1529
+ async applyForStaging(contact, purpose) {
1530
+ return this.stagingOnboarding('POST', '/applications', { contact, purpose });
1531
+ }
1532
+ async getStagingApplication() {
1533
+ return this.stagingOnboarding('GET', '/applications/me');
1534
+ }
1535
+ /** Rotates the wallet's staging key and installs it on this SDK instance. */
1536
+ async obtainStagingKey() {
1537
+ const result = await this.stagingOnboarding('POST', '/credential', {});
1538
+ this._stagingKey = result.staging_key;
1539
+ return result.staging_key;
1540
+ }
1541
+ async stagingOnboarding(method, path, body) {
1542
+ if (this._accessToken)
1543
+ throw new Error('Staging onboarding requires a wallet signer');
1544
+ const digest = ethers_1.ethers.sha256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(body ?? {}))).slice(2);
1545
+ const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
1546
+ if (!headers['x-agent-proof'])
1547
+ throw new Error('Could not sign staging application');
1548
+ const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
1549
+ method, headers: { ...headers, 'Content-Type': 'application/json' },
1550
+ signal: AbortSignal.timeout(10000),
1551
+ ...(body ? { body: JSON.stringify(body) } : {}),
1552
+ });
1553
+ const result = await response.json();
1554
+ if (!response.ok)
1555
+ throw new Error(result.error ?? `Staging request failed (${response.status})`);
1556
+ return result;
1557
+ }
1558
+ /** True for access-token sessions: credits-only, never signs. */
1559
+ get isAccessTokenSession() {
1560
+ return this._accessToken !== undefined;
1561
+ }
1445
1562
  /** Auth headers plus Content-Type for JSON-body requests. */
1446
1563
  jsonHeaders() {
1447
1564
  return { 'Content-Type': 'application/json', ...this.headers() };
@@ -1457,6 +1574,10 @@ class OneShot {
1457
1574
  */
1458
1575
  async signedReadHeaders(scope = 'read') {
1459
1576
  const base = this.headers();
1577
+ // The bearer token already binds the request to the agent (and is
1578
+ // revocable); the API accepts it in place of the signed proof.
1579
+ if (this._accessToken)
1580
+ return base;
1460
1581
  try {
1461
1582
  const agent = this.provider.address;
1462
1583
  const issuedAt = Math.floor(Date.now() / 1000);
@@ -1582,6 +1703,61 @@ class OneShot {
1582
1703
  const body = await response.json();
1583
1704
  return (body.data ?? body);
1584
1705
  }
1706
+ // ---------------------------------------------------------------------------
1707
+ // Access tokens (wallet sessions only)
1708
+ // ---------------------------------------------------------------------------
1709
+ assertWalletSession(action) {
1710
+ if (this._accessToken) {
1711
+ throw new errors_2.ValidationError(`Access tokens cannot ${action}; use a wallet session`, 'accessToken');
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Mint an access token for this agent: a revocable, credits-only credential
1716
+ * for code that runs where the wallet key cannot be (hosted MCP clients,
1717
+ * cloud runners). The plaintext is returned once and never stored.
1718
+ *
1719
+ * @example
1720
+ * ```typescript
1721
+ * const { token } = await agent.createAccessToken({ name: 'grok-bot' });
1722
+ * // hand `token` to the hosted client; it uses OneShot.create({ accessToken: token })
1723
+ * ```
1724
+ */
1725
+ async createAccessToken(opts = {}) {
1726
+ this.assertWalletSession('mint access tokens');
1727
+ const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens`, {
1728
+ method: 'POST',
1729
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1730
+ body: JSON.stringify(opts.name !== undefined ? { name: opts.name } : {}),
1731
+ });
1732
+ if (!response.ok)
1733
+ throw new errors_2.ToolError('Failed to create access token', response.status, await response.text());
1734
+ const body = await response.json();
1735
+ return body.data;
1736
+ }
1737
+ /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
1738
+ async listAccessTokens(opts = {}) {
1739
+ this.assertWalletSession('list access tokens');
1740
+ const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
1741
+ headers: await this.signedReadHeaders(),
1742
+ });
1743
+ if (!response.ok)
1744
+ throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text());
1745
+ const body = await response.json();
1746
+ return body.data.tokens;
1747
+ }
1748
+ /** Revoke an access token. Sessions using it fail with 401 from the next request. */
1749
+ async revokeAccessToken(id) {
1750
+ this.assertWalletSession('revoke access tokens');
1751
+ this.validate(id, 'id');
1752
+ const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
1753
+ method: 'DELETE',
1754
+ headers: await this.signedReadHeaders('write'),
1755
+ });
1756
+ if (!response.ok)
1757
+ throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text());
1758
+ const body = await response.json();
1759
+ return body.data;
1760
+ }
1585
1761
  /** Local fast-fail guard: throw when a quote total exceeds the caller's cap. */
1586
1762
  assertWithinMaxCost(total, maxCost) {
1587
1763
  if (maxCost && parseFloat(total) > maxCost) {
@@ -1625,6 +1801,9 @@ class OneShot {
1625
1801
  */
1626
1802
  async failFromResponse(message, response) {
1627
1803
  const text = await response.text();
1804
+ const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
1805
+ if (credits)
1806
+ throw credits;
1628
1807
  const rejection = response.status === 402 ? this.parsePaymentRejection(text) : undefined;
1629
1808
  if (rejection)
1630
1809
  throw rejection;
@@ -1633,6 +1812,76 @@ class OneShot {
1633
1812
  throw budget;
1634
1813
  throw new errors_2.ToolError(message, response.status, text);
1635
1814
  }
1815
+ /**
1816
+ * Map a 402 `insufficient_credits` body (access-token sessions) onto a typed
1817
+ * error. Any other 402 falls through to the payment-rejection / quote parse.
1818
+ */
1819
+ parseInsufficientCredits(text) {
1820
+ let body;
1821
+ try {
1822
+ body = typeof text === 'string' ? JSON.parse(text) : text;
1823
+ }
1824
+ catch {
1825
+ return undefined;
1826
+ }
1827
+ if (!body || body.error !== 'insufficient_credits')
1828
+ return undefined;
1829
+ const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
1830
+ 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));
1831
+ }
1832
+ /**
1833
+ * Access-token sessions never sign. When a paid call answers 402 the SDK
1834
+ * either got the server's `insufficient_credits` verdict (throw it) or a
1835
+ * quote (re-POST with `x-quote-id` and no payment: the server executes
1836
+ * when the quote's credits cover it, otherwise answers `insufficient_credits`
1837
+ * with the balance and shortfall, which failFromResponse turns into the
1838
+ * typed error). Server-decided on purpose — the SDK holds no ledger.
1839
+ */
1840
+ async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
1841
+ const credits = body ? this.parseInsufficientCredits(body) : undefined;
1842
+ if (credits)
1843
+ throw credits;
1844
+ const qid = quoteId ?? body?.context?.quote_id;
1845
+ if (!qid) {
1846
+ const amount = body?.payment_request?.amount;
1847
+ throw new errors_1.InsufficientCreditsError(`Credits do not cover ${amount ? `$${amount}` : 'this call'}; top up credits or use a wallet session`, amount ? parseFloat(amount) : undefined);
1848
+ }
1849
+ this.log(`Access-token session: retrying quote ${qid} against credits`);
1850
+ const retry = await this.makeRequest(endpoint, payload, undefined, qid, signal, timeoutMs, extraHeaders);
1851
+ if (retry.status === 402)
1852
+ await this.failFromResponse('Credits do not cover this call', retry);
1853
+ return retry;
1854
+ }
1855
+ /** GET /v1/agents/me with the token, for OneShot.create({ accessToken }) without an address. */
1856
+ static async resolveTokenAddress(config) {
1857
+ const TOKEN_RESOLVE_TIMEOUT_MS = 10000;
1858
+ const baseUrl = config.baseUrl ?? BASE_URL;
1859
+ let response;
1860
+ try {
1861
+ response = await fetch(`${baseUrl}/v1/agents/me`, {
1862
+ headers: {
1863
+ ...(config.defaultHeaders ?? {}),
1864
+ ...(config.stagingKey ? { 'X-Staging-Key': config.stagingKey } : {}),
1865
+ Authorization: `Bearer ${config.accessToken}`,
1866
+ 'X-OneShot-SDK-Version': SDK_VERSION,
1867
+ },
1868
+ // Bounded: with no signal, Node's fetch can wait minutes on a hung
1869
+ // connection and OneShot.create() would hang with it.
1870
+ signal: AbortSignal.timeout(TOKEN_RESOLVE_TIMEOUT_MS),
1871
+ });
1872
+ }
1873
+ catch (err) {
1874
+ throw new errors_2.ToolError(`Could not resolve the access token's agent (network): ${err}`, 0, String(err));
1875
+ }
1876
+ if (!response.ok) {
1877
+ throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text());
1878
+ }
1879
+ const json = await response.json();
1880
+ const address = json.data?.wallet_address;
1881
+ if (!address)
1882
+ throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json));
1883
+ return address;
1884
+ }
1636
1885
  /**
1637
1886
  * Map a 403 `budget_exceeded` body onto a typed error, so callers can catch
1638
1887
  * "my own budget stopped this" separately from an auth failure or a payment
@@ -1804,23 +2053,30 @@ class OneShot {
1804
2053
  const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
1805
2054
  // Fallback: parse legacy body format for amount display
1806
2055
  const data = await response.json();
1807
- const paymentInfo = {
1808
- protocol: 'x402',
1809
- network: accepted.network,
1810
- payTo: accepted.payTo,
1811
- amount: this.chargeAmount(accepted, data.payment_request?.amount),
1812
- currency: 'USD',
1813
- facilitator_url: this.baseUrl,
1814
- token: { address: accepted.asset, symbol: 'USDC', decimals: 6 }
1815
- };
1816
- this.log(`Payment required: ${paymentInfo.amount} USDC`);
1817
- this.assertWithinBudget(paymentInfo.amount);
1818
- this.checkAbortBeforePayment(signal);
1819
- context.phase = 'payment';
1820
- const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
1821
- this.checkAbortBeforePayment(signal);
1822
- context.phase = 'submission';
1823
- response = await this.makePaidRequest(signed, endpoint, payload, quoteId, signal, undefined, extraHeaders);
2056
+ if (this._accessToken) {
2057
+ // Credits-only session: never sign. Let the server settle it.
2058
+ this.checkAbortBeforePayment(signal);
2059
+ response = await this.retryAsCreditsSession(data, endpoint, payload, quoteId, signal, undefined, extraHeaders);
2060
+ }
2061
+ else {
2062
+ const paymentInfo = {
2063
+ protocol: 'x402',
2064
+ network: accepted.network,
2065
+ payTo: accepted.payTo,
2066
+ amount: this.chargeAmount(accepted, data.payment_request?.amount),
2067
+ currency: 'USD',
2068
+ facilitator_url: this.baseUrl,
2069
+ token: { address: accepted.asset, symbol: 'USDC', decimals: 6 }
2070
+ };
2071
+ this.log(`Payment required: ${paymentInfo.amount} USDC`);
2072
+ this.assertWithinBudget(paymentInfo.amount);
2073
+ this.checkAbortBeforePayment(signal);
2074
+ context.phase = 'payment';
2075
+ const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
2076
+ this.checkAbortBeforePayment(signal);
2077
+ context.phase = 'submission';
2078
+ response = await this.makePaidRequest(signed, endpoint, payload, quoteId, signal, undefined, extraHeaders);
2079
+ }
1824
2080
  }
1825
2081
  if (!response.ok) {
1826
2082
  await this.failFromResponse('Tool request failed', response);
@@ -1866,6 +2122,11 @@ class OneShot {
1866
2122
  cfg.onQuote(quoteData.context);
1867
2123
  this.assertWithinMaxCost(cfg.totalOf(quoteData.context), cfg.maxCost);
1868
2124
  this.assertWithinBudget(cfg.totalOf(quoteData.context));
2125
+ if (this._accessToken) {
2126
+ this.checkAbortBeforePayment(cfg.signal);
2127
+ const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
2128
+ return { context: quoteData.context, execResp };
2129
+ }
1869
2130
  const paymentInfo = {
1870
2131
  protocol: 'x402',
1871
2132
  network: `eip155:${quoteData.payment_request.chain_id}`,
@@ -2096,7 +2357,7 @@ class OneShot {
2096
2357
  settle(() => {
2097
2358
  cleanup();
2098
2359
  wait.via = 'ws';
2099
- reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code));
2360
+ reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
2100
2361
  });
2101
2362
  }
2102
2363
  else {
@@ -2187,7 +2448,7 @@ class OneShot {
2187
2448
  if (job.status === 'failed') {
2188
2449
  if (wait)
2189
2450
  wait.via = 'http';
2190
- throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code);
2451
+ throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
2191
2452
  }
2192
2453
  emit?.(String(job.status));
2193
2454
  retries = 0;
@@ -2487,6 +2748,11 @@ class OneShot {
2487
2748
  },
2488
2749
  } };
2489
2750
  }
2751
+ // Belt and braces: every paid path branches to retryAsCreditsSession
2752
+ // before reaching here in access-token mode.
2753
+ if (this._accessToken) {
2754
+ throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
2755
+ }
2490
2756
  // If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
2491
2757
  const reservation = await this.ensureUsdcBalance(paymentInfo);
2492
2758
  const now = Math.floor(Date.now() / 1000);