@oneshot-agent/sdk 0.18.0 → 0.19.1

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
@@ -48,7 +48,8 @@ var swap_1 = require("./swap");
48
48
  Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
49
49
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
50
50
  __exportStar(require("./errors"), exports);
51
- const SDK_VERSION = '0.16.2';
51
+ // Keep in sync with package.json `version`. Guarded by version.test.ts.
52
+ const SDK_VERSION = '0.19.1';
52
53
  // ============================================================================
53
54
  // Environment Configuration
54
55
  // ============================================================================
@@ -159,8 +160,11 @@ class OneShot {
159
160
  return this.executeToolRequest(`/v1/tools/${toolName}`, options);
160
161
  }
161
162
  async email(options) {
162
- this.validate(options.to, 'to');
163
- this.validate(options.subject, 'subject');
163
+ // to/subject are server-derived when replying to an inbound email.
164
+ if (!options.reply_to_email_id) {
165
+ this.validate(options.to, 'to');
166
+ this.validate(options.subject, 'subject');
167
+ }
164
168
  this.validate(options.body, 'body');
165
169
  // Rotation mode: when the caller passes neither from_domain nor
166
170
  // from_mailbox, omit from_address entirely so the server picks from
@@ -174,8 +178,9 @@ class OneShot {
174
178
  : `${options.from_mailbox ?? 'agent'}@${options.from_domain ?? 'oneshotagent.com'}`;
175
179
  const quote = await this.tool('email/quote', {
176
180
  ...(fromAddress ? { from_address: fromAddress } : {}),
177
- to_address: options.to,
178
- subject: options.subject,
181
+ ...(options.to !== undefined ? { to_address: options.to } : {}),
182
+ ...(options.subject !== undefined ? { subject: options.subject } : {}),
183
+ ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
179
184
  body: options.body,
180
185
  // Forward maxCost so the server-side X-Max-Cost-USDC header is set on
181
186
  // the quote fetch (executeToolRequest destructures + threads it).
@@ -188,17 +193,16 @@ class OneShot {
188
193
  // (e.g. a future API revision without the header check) or skipped (cap
189
194
  // un-set so the server returned a 200 with the quote and the caller still
190
195
  // wants the local guard to apply).
191
- if (options.maxCost && parseFloat(quote.total_cost) > options.maxCost) {
192
- throw new errors_1.OneShotError(`Quote $${quote.total_cost} exceeds maxCost $${options.maxCost}`);
193
- }
196
+ this.assertWithinMaxCost(quote.total_cost, options.maxCost);
194
197
  const resolvedFromAddress = fromAddress ?? quote.from_address;
195
198
  const payload = {
196
199
  // Server replays the locked address from the quote when from_address
197
200
  // is absent, but sending it anyway is harmless and forward-compatible
198
201
  // with future SDK versions that talk directly to /send without quoting.
199
202
  ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
200
- to_address: options.to,
201
- subject: options.subject,
203
+ ...(options.to !== undefined ? { to_address: options.to } : {}),
204
+ ...(options.subject !== undefined ? { subject: options.subject } : {}),
205
+ ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
202
206
  body: options.body,
203
207
  signal: options.signal,
204
208
  onStatusUpdate: options.onStatusUpdate,
@@ -210,6 +214,10 @@ class OneShot {
210
214
  if (options.attachments?.length) {
211
215
  payload.attachments = options.attachments;
212
216
  }
217
+ if (options.idempotencyKey) {
218
+ // Only the send call carries the key — the quote call has no side effects.
219
+ payload.idempotencyKey = options.idempotencyKey;
220
+ }
213
221
  return this.executeToolRequest('/v1/tools/email/send', payload, quote.quote_id);
214
222
  }
215
223
  /** List the caller's domain pool with warmup and rotation metadata. */
@@ -302,14 +310,11 @@ class OneShot {
302
310
  return this.tool('research/interactions', { ...options });
303
311
  }
304
312
  async inboxList(options = {}) {
305
- const params = new URLSearchParams();
306
- if (options.since)
307
- params.set('since', options.since);
308
- if (options.limit)
309
- params.set('limit', String(options.limit));
310
- if (options.include_body)
311
- params.set('include_body', 'true');
312
- const qs = params.toString();
313
+ const qs = this.buildQuery({
314
+ since: options.since || undefined,
315
+ limit: options.limit || undefined,
316
+ include_body: options.include_body ? 'true' : undefined,
317
+ });
313
318
  const response = await fetch(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
314
319
  headers: this.headers()
315
320
  });
@@ -342,28 +347,17 @@ class OneShot {
342
347
  variant_id: options.variant_id
343
348
  };
344
349
  // Commerce quotes can take up to 90s due to Rye API polling
345
- const quoteResp = await this.makeRequest('/v1/tools/commerce/buy', payload, undefined, undefined, options.signal, 120000, this.maxCostHeader(options.maxCost));
346
- if (quoteResp.status !== 402) {
347
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
348
- }
349
- const quoteData = await quoteResp.json();
350
- this.log(`Commerce quote: $${quoteData.context.total} for "${quoteData.context.product_title}"`);
351
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
352
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
353
- }
354
- const paymentInfo = {
355
- protocol: 'x402',
356
- network: `eip155:${quoteData.payment_request.chain_id}`,
357
- payTo: quoteData.payment_request.recipient,
358
- amount: quoteData.payment_request.amount,
359
- currency: 'USD',
360
- facilitator_url: this.baseUrl,
361
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
362
- };
363
- this.checkAbortBeforePayment(options.signal);
364
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/commerce/buy', payload, quoteData.context.quote_id, options.signal);
365
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
366
- const buyResp = await this.makeRequest('/v1/tools/commerce/buy', payload, auth, quoteData.context.quote_id, options.signal, 60000);
350
+ const { execResp: buyResp } = await this.runQuoteToPay({
351
+ endpoint: '/v1/tools/commerce/buy',
352
+ payload,
353
+ signal: options.signal,
354
+ maxCost: options.maxCost,
355
+ quoteTimeoutMs: 120000,
356
+ execTimeoutMs: 60000,
357
+ expectMsg: 'Expected 402 for quote',
358
+ totalOf: (ctx) => ctx.total,
359
+ onQuote: (ctx) => this.log(`Commerce quote: $${ctx.total} for "${ctx.product_title}"`),
360
+ });
367
361
  if (buyResp.status !== 202) {
368
362
  throw new errors_1.ToolError('Commerce buy failed', buyResp.status, await buyResp.text());
369
363
  }
@@ -415,9 +409,6 @@ class OneShot {
415
409
  const payload = {
416
410
  objective: options.objective,
417
411
  target_number: options.target_number,
418
- signal: options.signal,
419
- onStatusUpdate: options.onStatusUpdate,
420
- wait: options.wait
421
412
  };
422
413
  if (options.caller_persona)
423
414
  payload.caller_persona = options.caller_persona;
@@ -425,40 +416,25 @@ class OneShot {
425
416
  payload.context = options.context;
426
417
  if (options.max_duration_minutes)
427
418
  payload.max_duration_minutes = options.max_duration_minutes;
428
- // Voice uses quote-to-pay flow (402 -> payment -> 202)
429
- const quoteResp = await this.makeRequest('/v1/tools/voice/call', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
430
- // Handle error responses before expecting 402
431
- if (quoteResp.status === 400) {
432
- const errorData = await quoteResp.json();
433
- if (errorData.error === 'content_blocked') {
434
- throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
435
- }
436
- if (errorData.error === 'emergency_number_blocked') {
437
- throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
438
- }
439
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
440
- }
441
- if (quoteResp.status !== 402) {
442
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
443
- }
444
- const quoteData = await quoteResp.json();
445
- this.log(`Voice quote: $${quoteData.context.total} for ${quoteData.context.estimated_duration_minutes}min call`);
446
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
447
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
448
- }
449
- const paymentInfo = {
450
- protocol: 'x402',
451
- network: `eip155:${quoteData.payment_request.chain_id}`,
452
- payTo: quoteData.payment_request.recipient,
453
- amount: quoteData.payment_request.amount,
454
- currency: 'USD',
455
- facilitator_url: this.baseUrl,
456
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
457
- };
458
- this.checkAbortBeforePayment(options.signal);
459
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/voice/call', payload, quoteData.context.quote_id, options.signal);
460
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
461
- const callResp = await this.makeRequest('/v1/tools/voice/call', payload, auth, quoteData.context.quote_id, options.signal);
419
+ const { execResp: callResp } = await this.runQuoteToPay({
420
+ endpoint: '/v1/tools/voice/call',
421
+ payload,
422
+ signal: options.signal,
423
+ maxCost: options.maxCost,
424
+ expectMsg: 'Expected 402 for quote',
425
+ totalOf: (ctx) => ctx.total,
426
+ onQuote: (ctx) => this.log(`Voice quote: $${ctx.total} for ${ctx.estimated_duration_minutes}min call`),
427
+ on400: async (resp) => {
428
+ const errorData = await resp.json();
429
+ if (errorData.error === 'content_blocked') {
430
+ throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
431
+ }
432
+ if (errorData.error === 'emergency_number_blocked') {
433
+ throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
434
+ }
435
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
436
+ },
437
+ });
462
438
  if (callResp.status !== 202) {
463
439
  throw new errors_1.ToolError('Voice call initiation failed', callResp.status, await callResp.text());
464
440
  }
@@ -501,44 +477,27 @@ class OneShot {
501
477
  const payload = {
502
478
  message: options.message,
503
479
  to_number: options.to_number,
504
- signal: options.signal,
505
- onStatusUpdate: options.onStatusUpdate,
506
- wait: options.wait
507
480
  };
508
481
  // SMS uses quote-to-pay flow (402 -> payment -> 202)
509
- const quoteResp = await this.makeRequest('/v1/tools/sms/send', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
510
- // Handle error responses before expecting 402
511
- if (quoteResp.status === 400) {
512
- const errorData = await quoteResp.json();
513
- if (errorData.error === 'content_blocked') {
514
- throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
515
- }
516
- if (errorData.error === 'emergency_number_blocked') {
517
- throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
518
- }
519
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
520
- }
521
- if (quoteResp.status !== 402) {
522
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
523
- }
524
- const quoteData = await quoteResp.json();
525
- this.log(`SMS quote: $${quoteData.context.total} for ${quoteData.context.segment_count} segment(s) to ${recipientCount} recipient(s)`);
526
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
527
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
528
- }
529
- const paymentInfo = {
530
- protocol: 'x402',
531
- network: `eip155:${quoteData.payment_request.chain_id}`,
532
- payTo: quoteData.payment_request.recipient,
533
- amount: quoteData.payment_request.amount,
534
- currency: 'USD',
535
- facilitator_url: this.baseUrl,
536
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
537
- };
538
- this.checkAbortBeforePayment(options.signal);
539
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/sms/send', payload, quoteData.context.quote_id, options.signal);
540
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
541
- const sendResp = await this.makeRequest('/v1/tools/sms/send', payload, auth, quoteData.context.quote_id, options.signal);
482
+ const { execResp: sendResp } = await this.runQuoteToPay({
483
+ endpoint: '/v1/tools/sms/send',
484
+ payload,
485
+ signal: options.signal,
486
+ maxCost: options.maxCost,
487
+ expectMsg: 'Expected 402 for quote',
488
+ totalOf: (ctx) => ctx.total,
489
+ onQuote: (ctx) => this.log(`SMS quote: $${ctx.total} for ${ctx.segment_count} segment(s) to ${recipientCount} recipient(s)`),
490
+ on400: async (resp) => {
491
+ const errorData = await resp.json();
492
+ if (errorData.error === 'content_blocked') {
493
+ throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
494
+ }
495
+ if (errorData.error === 'emergency_number_blocked') {
496
+ throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
497
+ }
498
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
499
+ },
500
+ });
542
501
  if (sendResp.status !== 202) {
543
502
  throw new errors_1.ToolError('SMS send failed', sendResp.status, await sendResp.text());
544
503
  }
@@ -575,9 +534,6 @@ class OneShot {
575
534
  const payload = {
576
535
  type: options.type ?? 'saas',
577
536
  product: options.product,
578
- signal: options.signal,
579
- onStatusUpdate: options.onStatusUpdate,
580
- wait: options.wait
581
537
  };
582
538
  if (options.source_url)
583
539
  payload.source_url = options.source_url;
@@ -593,34 +549,24 @@ class OneShot {
593
549
  payload.domain = options.domain;
594
550
  if (options.build_id)
595
551
  payload.build_id = options.build_id;
596
- // Build uses quote-to-pay flow (402 -> payment -> 202)
597
- const quoteResp = await this.makeRequest('/v1/tools/build', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
598
- if (quoteResp.status === 400) {
599
- const errorData = await quoteResp.json();
600
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
601
- }
602
- if (quoteResp.status !== 402) {
603
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
604
- }
605
- const quoteData = await quoteResp.json();
606
- this.log(`Build quote: $${quoteData.context.pricing.total} for "${quoteData.context.product_name}"`);
607
- this.log(`Type: ${quoteData.context.analysis.inferred_type}, Sections: ${quoteData.context.analysis.estimated_sections}`);
608
- if (options.maxCost && parseFloat(quoteData.context.pricing.total) > options.maxCost) {
609
- throw new errors_1.OneShotError(`Quote $${quoteData.context.pricing.total} exceeds maxCost $${options.maxCost}`);
610
- }
611
- const paymentInfo = {
612
- protocol: 'x402',
613
- network: `eip155:${quoteData.payment_request.chain_id}`,
614
- payTo: quoteData.payment_request.recipient,
615
- amount: quoteData.payment_request.amount,
616
- currency: 'USD',
617
- facilitator_url: this.baseUrl,
618
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
619
- };
620
- this.checkAbortBeforePayment(options.signal);
621
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/build', payload, quoteData.context.quote_id, options.signal);
622
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
623
- const buildResp = await this.makeRequest('/v1/tools/build', payload, auth, quoteData.context.quote_id, options.signal);
552
+ const { execResp: buildResp } = await this.runQuoteToPay({
553
+ endpoint: '/v1/tools/build',
554
+ payload,
555
+ signal: options.signal,
556
+ maxCost: options.maxCost,
557
+ // Build analysis can be slow server-side; bound the quote leg client-side.
558
+ quoteTimeoutMs: 120000,
559
+ expectMsg: 'Expected 402 for quote',
560
+ totalOf: (ctx) => ctx.pricing.total,
561
+ onQuote: (ctx) => {
562
+ this.log(`Build quote: $${ctx.pricing.total} for "${ctx.product_name}"`);
563
+ this.log(`Type: ${ctx.analysis.inferred_type}, Sections: ${ctx.analysis.estimated_sections}`);
564
+ },
565
+ on400: async (resp) => {
566
+ const errorData = await resp.json();
567
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
568
+ },
569
+ });
624
570
  if (buildResp.status !== 202) {
625
571
  throw new errors_1.ToolError('Build initiation failed', buildResp.status, await buildResp.text());
626
572
  }
@@ -653,9 +599,6 @@ class OneShot {
653
599
  }
654
600
  const payload = {
655
601
  task: options.task,
656
- signal: options.signal,
657
- onStatusUpdate: options.onStatusUpdate,
658
- wait: options.wait
659
602
  };
660
603
  if (options.output_schema)
661
604
  payload.output_schema = options.output_schema;
@@ -671,33 +614,21 @@ class OneShot {
671
614
  payload.secrets = options.secrets;
672
615
  if (options.max_steps)
673
616
  payload.max_steps = options.max_steps;
674
- // Browser uses quote-to-pay flow (402 -> payment -> 202)
675
- const quoteResp = await this.makeRequest('/v1/tools/browser', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
676
- if (quoteResp.status === 400) {
677
- const errorData = await quoteResp.json();
678
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
679
- }
680
- if (quoteResp.status !== 402) {
681
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
682
- }
683
- const quoteData = await quoteResp.json();
684
- this.log(`Browser quote: $${quoteData.context.estimated_cost} for ~${quoteData.context.estimated_steps} steps`);
685
- if (options.maxCost && parseFloat(quoteData.context.estimated_cost) > options.maxCost) {
686
- throw new errors_1.OneShotError(`Quote $${quoteData.context.estimated_cost} exceeds maxCost $${options.maxCost}`);
687
- }
688
- const paymentInfo = {
689
- protocol: 'x402',
690
- network: `eip155:${quoteData.payment_request.chain_id}`,
691
- payTo: quoteData.payment_request.recipient,
692
- amount: quoteData.payment_request.amount,
693
- currency: 'USD',
694
- facilitator_url: this.baseUrl,
695
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
696
- };
697
- this.checkAbortBeforePayment(options.signal);
698
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/browser', payload, quoteData.context.quote_id, options.signal);
699
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
700
- const execResp = await this.makeRequest('/v1/tools/browser', payload, auth, quoteData.context.quote_id, options.signal);
617
+ const { execResp } = await this.runQuoteToPay({
618
+ endpoint: '/v1/tools/browser',
619
+ payload,
620
+ signal: options.signal,
621
+ maxCost: options.maxCost,
622
+ // Browser analysis can be slow server-side; bound the quote leg client-side.
623
+ quoteTimeoutMs: 120000,
624
+ expectMsg: 'Expected 402 for quote',
625
+ totalOf: (ctx) => ctx.estimated_cost,
626
+ onQuote: (ctx) => this.log(`Browser quote: $${ctx.estimated_cost} for ~${ctx.estimated_steps} steps`),
627
+ on400: async (resp) => {
628
+ const errorData = await resp.json();
629
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
630
+ },
631
+ });
701
632
  if (execResp.status !== 202) {
702
633
  throw new errors_1.ToolError('Browser task initiation failed', execResp.status, await execResp.text());
703
634
  }
@@ -721,7 +652,7 @@ class OneShot {
721
652
  this.validate(name, 'name');
722
653
  const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
723
654
  method: 'POST',
724
- headers: { 'Content-Type': 'application/json', ...this.headers() },
655
+ headers: this.jsonHeaders(),
725
656
  body: JSON.stringify({ name }),
726
657
  });
727
658
  if (!response.ok) {
@@ -802,14 +733,11 @@ class OneShot {
802
733
  * ```
803
734
  */
804
735
  async smsInboxList(options = {}) {
805
- const params = new URLSearchParams();
806
- if (options.since)
807
- params.set('since', options.since);
808
- if (options.limit)
809
- params.set('limit', String(options.limit));
810
- if (options.from)
811
- params.set('from', options.from);
812
- const qs = params.toString();
736
+ const qs = this.buildQuery({
737
+ since: options.since || undefined,
738
+ limit: options.limit || undefined,
739
+ from: options.from || undefined,
740
+ });
813
741
  const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
814
742
  headers: this.headers()
815
743
  });
@@ -853,12 +781,10 @@ class OneShot {
853
781
  * ```
854
782
  */
855
783
  async notifications(options = {}) {
856
- const params = new URLSearchParams();
857
- if (options.unread)
858
- params.set('unread', 'true');
859
- if (options.limit)
860
- params.set('limit', String(options.limit));
861
- const qs = params.toString();
784
+ const qs = this.buildQuery({
785
+ unread: options.unread ? 'true' : undefined,
786
+ limit: options.limit || undefined,
787
+ });
862
788
  const response = await fetch(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
863
789
  headers: this.headers()
864
790
  });
@@ -950,36 +876,24 @@ class OneShot {
950
876
  payload.soul_service_slug = options.soul_service_slug;
951
877
  if (options.schedule)
952
878
  payload.schedule = options.schedule;
953
- // First call: get quote (402)
954
- const quoteResp = await this.makeRequest('/v1/compute', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
955
- if (quoteResp.status === 400) {
956
- const errorData = await quoteResp.json();
957
- if (errorData.error === 'content_blocked') {
958
- throw new errors_1.ContentBlockedError(errorData.message, []);
959
- }
960
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
961
- }
962
- if (quoteResp.status !== 402) {
963
- throw new errors_1.ToolError('Expected 402 for compute quote', quoteResp.status, await quoteResp.text());
964
- }
965
- const quoteData = await quoteResp.json();
966
- this.log(`Compute quote: $${quoteData.context.total_budget} — ${quoteData.context.objective_summary}`);
967
- if (options.maxCost && parseFloat(quoteData.context.total_budget) > options.maxCost) {
968
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total_budget} exceeds maxCost $${options.maxCost}`);
969
- }
970
- const paymentInfo = {
971
- protocol: 'x402',
972
- network: `eip155:${quoteData.payment_request.chain_id}`,
973
- payTo: quoteData.payment_request.recipient,
974
- amount: quoteData.payment_request.amount,
975
- currency: 'USD',
976
- facilitator_url: this.baseUrl,
977
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
978
- };
979
- this.checkAbortBeforePayment(options.signal);
980
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/compute', payload, quoteData.context.quote_id, options.signal);
981
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
982
- const createResp = await this.makeRequest('/v1/compute', payload, auth, quoteData.context.quote_id, options.signal);
879
+ // Compute returns the goal directly on 202 — no job polling, unlike the
880
+ // other paid tools.
881
+ const { execResp: createResp } = await this.runQuoteToPay({
882
+ endpoint: '/v1/compute',
883
+ payload,
884
+ signal: options.signal,
885
+ maxCost: options.maxCost,
886
+ expectMsg: 'Expected 402 for compute quote',
887
+ totalOf: (ctx) => ctx.total_budget,
888
+ onQuote: (ctx) => this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`),
889
+ on400: async (resp) => {
890
+ const errorData = await resp.json();
891
+ if (errorData.error === 'content_blocked') {
892
+ throw new errors_1.ContentBlockedError(errorData.message, []);
893
+ }
894
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
895
+ },
896
+ });
983
897
  if (createResp.status !== 202) {
984
898
  throw new errors_1.ToolError('Compute goal creation failed', createResp.status, await createResp.text());
985
899
  }
@@ -1066,7 +980,7 @@ class OneShot {
1066
980
  this.validate(goalId, 'goalId');
1067
981
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1068
982
  method: 'POST',
1069
- headers: { 'Content-Type': 'application/json', ...this.headers() },
983
+ headers: this.jsonHeaders(),
1070
984
  body: JSON.stringify({ reason })
1071
985
  });
1072
986
  if (response.status === 404) {
@@ -1095,7 +1009,7 @@ class OneShot {
1095
1009
  this.validate(input.task_id, 'task_id');
1096
1010
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1097
1011
  method: 'POST',
1098
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1012
+ headers: this.jsonHeaders(),
1099
1013
  body: JSON.stringify(input)
1100
1014
  });
1101
1015
  if (response.status === 404) {
@@ -1119,7 +1033,7 @@ class OneShot {
1119
1033
  this.validate(goalId, 'goalId');
1120
1034
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1121
1035
  method: 'POST',
1122
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1036
+ headers: this.jsonHeaders(),
1123
1037
  body: JSON.stringify({ reason })
1124
1038
  });
1125
1039
  if (!response.ok) {
@@ -1141,7 +1055,7 @@ class OneShot {
1141
1055
  this.validate(goalId, 'goalId');
1142
1056
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1143
1057
  method: 'POST',
1144
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1058
+ headers: this.jsonHeaders(),
1145
1059
  body: JSON.stringify({})
1146
1060
  });
1147
1061
  if (!response.ok) {
@@ -1210,10 +1124,7 @@ class OneShot {
1210
1124
  * ```
1211
1125
  */
1212
1126
  async spendBreakdown(options) {
1213
- const params = new URLSearchParams();
1214
- if (options?.period)
1215
- params.set('period', String(options.period));
1216
- const qs = params.toString();
1127
+ const qs = this.buildQuery({ period: options?.period || undefined });
1217
1128
  const response = await fetch(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
1218
1129
  headers: this.headers()
1219
1130
  });
@@ -1232,10 +1143,7 @@ class OneShot {
1232
1143
  * ```
1233
1144
  */
1234
1145
  async rocs(options) {
1235
- const params = new URLSearchParams();
1236
- if (options?.period)
1237
- params.set('period', String(options.period));
1238
- const qs = params.toString();
1146
+ const qs = this.buildQuery({ period: options?.period || undefined });
1239
1147
  const response = await fetch(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
1240
1148
  headers: this.headers()
1241
1149
  });
@@ -1256,14 +1164,11 @@ class OneShot {
1256
1164
  * ```
1257
1165
  */
1258
1166
  async receiptsList(options) {
1259
- const params = new URLSearchParams();
1260
- if (options?.period)
1261
- params.set('period', String(options.period));
1262
- if (options?.category)
1263
- params.set('category', options.category);
1264
- if (options?.limit)
1265
- params.set('limit', String(options.limit));
1266
- const qs = params.toString();
1167
+ const qs = this.buildQuery({
1168
+ period: options?.period || undefined,
1169
+ category: options?.category || undefined,
1170
+ limit: options?.limit || undefined,
1171
+ });
1267
1172
  const response = await fetch(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
1268
1173
  headers: this.headers()
1269
1174
  });
@@ -1315,6 +1220,26 @@ class OneShot {
1315
1220
  'X-OneShot-SDK-Version': SDK_VERSION
1316
1221
  };
1317
1222
  }
1223
+ /** Auth headers plus Content-Type for JSON-body requests. */
1224
+ jsonHeaders() {
1225
+ return { 'Content-Type': 'application/json', ...this.headers() };
1226
+ }
1227
+ /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
1228
+ * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
1229
+ buildQuery(params) {
1230
+ const qs = new URLSearchParams();
1231
+ for (const [k, v] of Object.entries(params)) {
1232
+ if (v != null)
1233
+ qs.set(k, String(v));
1234
+ }
1235
+ return qs.toString();
1236
+ }
1237
+ /** Local fast-fail guard: throw when a quote total exceeds the caller's cap. */
1238
+ assertWithinMaxCost(total, maxCost) {
1239
+ if (maxCost && parseFloat(total) > maxCost) {
1240
+ throw new errors_1.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
1241
+ }
1242
+ }
1318
1243
  /**
1319
1244
  * Header that asks the API to reject the request when the computed quote
1320
1245
  * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
@@ -1327,9 +1252,22 @@ class OneShot {
1327
1252
  return undefined;
1328
1253
  return { 'X-Max-Cost-USDC': maxCost.toString() };
1329
1254
  }
1255
+ /**
1256
+ * Idempotency-Key header, sent on both legs (pre-402 and paid retry) so
1257
+ * the server can replay a cached result instead of double-charging and
1258
+ * double-executing on a client retry.
1259
+ */
1260
+ idempotencyHeader(idempotencyKey) {
1261
+ if (!idempotencyKey)
1262
+ return undefined;
1263
+ return { 'Idempotency-Key': idempotencyKey };
1264
+ }
1330
1265
  async executeToolRequest(endpoint, options, quoteId) {
1331
- const { signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, maxCost, ...payload } = options;
1332
- const extraHeaders = this.maxCostHeader(maxCost);
1266
+ const { signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, maxCost, ...payload } = options;
1267
+ const extraHeaders = {
1268
+ ...this.maxCostHeader(maxCost),
1269
+ ...this.idempotencyHeader(idempotencyKey),
1270
+ };
1333
1271
  // Validate memo
1334
1272
  if (payload.memo !== undefined) {
1335
1273
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
@@ -1394,15 +1332,55 @@ class OneShot {
1394
1332
  }
1395
1333
  return (result.data ?? result);
1396
1334
  }
1335
+ /**
1336
+ * Shared x402 quote-to-pay flow for paid tools. Fetches the 402 quote, runs
1337
+ * any per-tool 400 handling, enforces maxCost, signs the payment auth, and
1338
+ * POSTs the paid request. Returns the parsed quote context plus the raw paid
1339
+ * Response — the caller owns the post-202 handling (poll a job vs. return the
1340
+ * body directly), since that differs per tool.
1341
+ */
1342
+ async runQuoteToPay(cfg) {
1343
+ const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, this.maxCostHeader(cfg.maxCost));
1344
+ if (quoteResp.status === 400 && cfg.on400) {
1345
+ await cfg.on400(quoteResp);
1346
+ }
1347
+ if (quoteResp.status !== 402) {
1348
+ throw new errors_1.ToolError(cfg.expectMsg, quoteResp.status, await quoteResp.text());
1349
+ }
1350
+ const quoteData = await quoteResp.json();
1351
+ cfg.onQuote(quoteData.context);
1352
+ this.assertWithinMaxCost(cfg.totalOf(quoteData.context), cfg.maxCost);
1353
+ const paymentInfo = {
1354
+ protocol: 'x402',
1355
+ network: `eip155:${quoteData.payment_request.chain_id}`,
1356
+ payTo: quoteData.payment_request.recipient,
1357
+ amount: quoteData.payment_request.amount,
1358
+ currency: 'USD',
1359
+ facilitator_url: this.baseUrl,
1360
+ token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
1361
+ };
1362
+ this.checkAbortBeforePayment(cfg.signal);
1363
+ const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal);
1364
+ const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
1365
+ const execResp = await this.makeRequest(cfg.endpoint, cfg.payload, auth, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
1366
+ return { context: quoteData.context, execResp };
1367
+ }
1397
1368
  async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts) {
1398
- // Try WebSocket push first, fall back to HTTP polling
1369
+ // Try WebSocket push first, fall back to HTTP polling. Both phases share a
1370
+ // single deadline so the combined wait never exceeds the caller's timeout —
1371
+ // previously the WS cap (≤180s) plus a fresh full HTTP timeout could total
1372
+ // up to ~1.6× timeoutSec (e.g. ~480s for a 300s request).
1373
+ const deadline = timeoutSec != null ? Date.now() + timeoutSec * 1000 : undefined;
1399
1374
  let result;
1400
1375
  try {
1401
1376
  result = await this.waitViaWebSocket(requestId, timeoutSec, signal, onStatusUpdate);
1402
1377
  }
1403
1378
  catch {
1404
1379
  this.log('WebSocket unavailable, falling back to HTTP polling');
1405
- result = await this.pollJobHttp(requestId, timeoutSec, signal, onStatusUpdate);
1380
+ const remainingSec = deadline != null
1381
+ ? Math.max(1, Math.ceil((deadline - Date.now()) / 1000))
1382
+ : undefined;
1383
+ result = await this.pollJobHttp(requestId, remainingSec, signal, onStatusUpdate);
1406
1384
  }
1407
1385
  // Optional second phase: keep polling for the async phone-reveal webhook.
1408
1386
  // Only kicks in when the caller explicitly opts in AND the result still
@@ -1579,7 +1557,7 @@ class OneShot {
1579
1557
  else if (msg.status === 'failed') {
1580
1558
  settle(() => {
1581
1559
  cleanup();
1582
- reject(new errors_1.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown')));
1560
+ reject(new errors_1.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code));
1583
1561
  });
1584
1562
  }
1585
1563
  else {
@@ -1635,7 +1613,7 @@ class OneShot {
1635
1613
  return result;
1636
1614
  }
1637
1615
  if (job.status === 'failed') {
1638
- throw new errors_1.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'));
1616
+ throw new errors_1.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code);
1639
1617
  }
1640
1618
  onStatusUpdate?.(job.status, requestId);
1641
1619
  retries = 0;