@oneshot-agent/sdk 0.19.0 → 0.19.2

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.2';
52
53
  // ============================================================================
53
54
  // Environment Configuration
54
55
  // ============================================================================
@@ -192,9 +193,7 @@ class OneShot {
192
193
  // (e.g. a future API revision without the header check) or skipped (cap
193
194
  // un-set so the server returned a 200 with the quote and the caller still
194
195
  // wants the local guard to apply).
195
- if (options.maxCost && parseFloat(quote.total_cost) > options.maxCost) {
196
- throw new errors_1.OneShotError(`Quote $${quote.total_cost} exceeds maxCost $${options.maxCost}`);
197
- }
196
+ this.assertWithinMaxCost(quote.total_cost, options.maxCost);
198
197
  const resolvedFromAddress = fromAddress ?? quote.from_address;
199
198
  const payload = {
200
199
  // Server replays the locked address from the quote when from_address
@@ -311,14 +310,11 @@ class OneShot {
311
310
  return this.tool('research/interactions', { ...options });
312
311
  }
313
312
  async inboxList(options = {}) {
314
- const params = new URLSearchParams();
315
- if (options.since)
316
- params.set('since', options.since);
317
- if (options.limit)
318
- params.set('limit', String(options.limit));
319
- if (options.include_body)
320
- params.set('include_body', 'true');
321
- 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
+ });
322
318
  const response = await fetch(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
323
319
  headers: this.headers()
324
320
  });
@@ -351,28 +347,17 @@ class OneShot {
351
347
  variant_id: options.variant_id
352
348
  };
353
349
  // Commerce quotes can take up to 90s due to Rye API polling
354
- const quoteResp = await this.makeRequest('/v1/tools/commerce/buy', payload, undefined, undefined, options.signal, 120000, this.maxCostHeader(options.maxCost));
355
- if (quoteResp.status !== 402) {
356
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
357
- }
358
- const quoteData = await quoteResp.json();
359
- this.log(`Commerce quote: $${quoteData.context.total} for "${quoteData.context.product_title}"`);
360
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
361
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
362
- }
363
- const paymentInfo = {
364
- protocol: 'x402',
365
- network: `eip155:${quoteData.payment_request.chain_id}`,
366
- payTo: quoteData.payment_request.recipient,
367
- amount: quoteData.payment_request.amount,
368
- currency: 'USD',
369
- facilitator_url: this.baseUrl,
370
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
371
- };
372
- this.checkAbortBeforePayment(options.signal);
373
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/commerce/buy', payload, quoteData.context.quote_id, options.signal);
374
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
375
- 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
+ });
376
361
  if (buyResp.status !== 202) {
377
362
  throw new errors_1.ToolError('Commerce buy failed', buyResp.status, await buyResp.text());
378
363
  }
@@ -424,9 +409,6 @@ class OneShot {
424
409
  const payload = {
425
410
  objective: options.objective,
426
411
  target_number: options.target_number,
427
- signal: options.signal,
428
- onStatusUpdate: options.onStatusUpdate,
429
- wait: options.wait
430
412
  };
431
413
  if (options.caller_persona)
432
414
  payload.caller_persona = options.caller_persona;
@@ -434,40 +416,25 @@ class OneShot {
434
416
  payload.context = options.context;
435
417
  if (options.max_duration_minutes)
436
418
  payload.max_duration_minutes = options.max_duration_minutes;
437
- // Voice uses quote-to-pay flow (402 -> payment -> 202)
438
- const quoteResp = await this.makeRequest('/v1/tools/voice/call', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
439
- // Handle error responses before expecting 402
440
- if (quoteResp.status === 400) {
441
- const errorData = await quoteResp.json();
442
- if (errorData.error === 'content_blocked') {
443
- throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
444
- }
445
- if (errorData.error === 'emergency_number_blocked') {
446
- throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
447
- }
448
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
449
- }
450
- if (quoteResp.status !== 402) {
451
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
452
- }
453
- const quoteData = await quoteResp.json();
454
- this.log(`Voice quote: $${quoteData.context.total} for ${quoteData.context.estimated_duration_minutes}min call`);
455
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
456
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
457
- }
458
- const paymentInfo = {
459
- protocol: 'x402',
460
- network: `eip155:${quoteData.payment_request.chain_id}`,
461
- payTo: quoteData.payment_request.recipient,
462
- amount: quoteData.payment_request.amount,
463
- currency: 'USD',
464
- facilitator_url: this.baseUrl,
465
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
466
- };
467
- this.checkAbortBeforePayment(options.signal);
468
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/voice/call', payload, quoteData.context.quote_id, options.signal);
469
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
470
- 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
+ });
471
438
  if (callResp.status !== 202) {
472
439
  throw new errors_1.ToolError('Voice call initiation failed', callResp.status, await callResp.text());
473
440
  }
@@ -510,44 +477,27 @@ class OneShot {
510
477
  const payload = {
511
478
  message: options.message,
512
479
  to_number: options.to_number,
513
- signal: options.signal,
514
- onStatusUpdate: options.onStatusUpdate,
515
- wait: options.wait
516
480
  };
517
481
  // SMS uses quote-to-pay flow (402 -> payment -> 202)
518
- const quoteResp = await this.makeRequest('/v1/tools/sms/send', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
519
- // Handle error responses before expecting 402
520
- if (quoteResp.status === 400) {
521
- const errorData = await quoteResp.json();
522
- if (errorData.error === 'content_blocked') {
523
- throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
524
- }
525
- if (errorData.error === 'emergency_number_blocked') {
526
- throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
527
- }
528
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
529
- }
530
- if (quoteResp.status !== 402) {
531
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
532
- }
533
- const quoteData = await quoteResp.json();
534
- this.log(`SMS quote: $${quoteData.context.total} for ${quoteData.context.segment_count} segment(s) to ${recipientCount} recipient(s)`);
535
- if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
536
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
537
- }
538
- const paymentInfo = {
539
- protocol: 'x402',
540
- network: `eip155:${quoteData.payment_request.chain_id}`,
541
- payTo: quoteData.payment_request.recipient,
542
- amount: quoteData.payment_request.amount,
543
- currency: 'USD',
544
- facilitator_url: this.baseUrl,
545
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
546
- };
547
- this.checkAbortBeforePayment(options.signal);
548
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/sms/send', payload, quoteData.context.quote_id, options.signal);
549
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
550
- 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
+ });
551
501
  if (sendResp.status !== 202) {
552
502
  throw new errors_1.ToolError('SMS send failed', sendResp.status, await sendResp.text());
553
503
  }
@@ -584,9 +534,6 @@ class OneShot {
584
534
  const payload = {
585
535
  type: options.type ?? 'saas',
586
536
  product: options.product,
587
- signal: options.signal,
588
- onStatusUpdate: options.onStatusUpdate,
589
- wait: options.wait
590
537
  };
591
538
  if (options.source_url)
592
539
  payload.source_url = options.source_url;
@@ -602,34 +549,24 @@ class OneShot {
602
549
  payload.domain = options.domain;
603
550
  if (options.build_id)
604
551
  payload.build_id = options.build_id;
605
- // Build uses quote-to-pay flow (402 -> payment -> 202)
606
- const quoteResp = await this.makeRequest('/v1/tools/build', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
607
- if (quoteResp.status === 400) {
608
- const errorData = await quoteResp.json();
609
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
610
- }
611
- if (quoteResp.status !== 402) {
612
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
613
- }
614
- const quoteData = await quoteResp.json();
615
- this.log(`Build quote: $${quoteData.context.pricing.total} for "${quoteData.context.product_name}"`);
616
- this.log(`Type: ${quoteData.context.analysis.inferred_type}, Sections: ${quoteData.context.analysis.estimated_sections}`);
617
- if (options.maxCost && parseFloat(quoteData.context.pricing.total) > options.maxCost) {
618
- throw new errors_1.OneShotError(`Quote $${quoteData.context.pricing.total} exceeds maxCost $${options.maxCost}`);
619
- }
620
- const paymentInfo = {
621
- protocol: 'x402',
622
- network: `eip155:${quoteData.payment_request.chain_id}`,
623
- payTo: quoteData.payment_request.recipient,
624
- amount: quoteData.payment_request.amount,
625
- currency: 'USD',
626
- facilitator_url: this.baseUrl,
627
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
628
- };
629
- this.checkAbortBeforePayment(options.signal);
630
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/build', payload, quoteData.context.quote_id, options.signal);
631
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
632
- 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
+ });
633
570
  if (buildResp.status !== 202) {
634
571
  throw new errors_1.ToolError('Build initiation failed', buildResp.status, await buildResp.text());
635
572
  }
@@ -662,9 +599,6 @@ class OneShot {
662
599
  }
663
600
  const payload = {
664
601
  task: options.task,
665
- signal: options.signal,
666
- onStatusUpdate: options.onStatusUpdate,
667
- wait: options.wait
668
602
  };
669
603
  if (options.output_schema)
670
604
  payload.output_schema = options.output_schema;
@@ -680,33 +614,21 @@ class OneShot {
680
614
  payload.secrets = options.secrets;
681
615
  if (options.max_steps)
682
616
  payload.max_steps = options.max_steps;
683
- // Browser uses quote-to-pay flow (402 -> payment -> 202)
684
- const quoteResp = await this.makeRequest('/v1/tools/browser', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
685
- if (quoteResp.status === 400) {
686
- const errorData = await quoteResp.json();
687
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
688
- }
689
- if (quoteResp.status !== 402) {
690
- throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
691
- }
692
- const quoteData = await quoteResp.json();
693
- this.log(`Browser quote: $${quoteData.context.estimated_cost} for ~${quoteData.context.estimated_steps} steps`);
694
- if (options.maxCost && parseFloat(quoteData.context.estimated_cost) > options.maxCost) {
695
- throw new errors_1.OneShotError(`Quote $${quoteData.context.estimated_cost} exceeds maxCost $${options.maxCost}`);
696
- }
697
- const paymentInfo = {
698
- protocol: 'x402',
699
- network: `eip155:${quoteData.payment_request.chain_id}`,
700
- payTo: quoteData.payment_request.recipient,
701
- amount: quoteData.payment_request.amount,
702
- currency: 'USD',
703
- facilitator_url: this.baseUrl,
704
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
705
- };
706
- this.checkAbortBeforePayment(options.signal);
707
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/tools/browser', payload, quoteData.context.quote_id, options.signal);
708
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
709
- 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
+ });
710
632
  if (execResp.status !== 202) {
711
633
  throw new errors_1.ToolError('Browser task initiation failed', execResp.status, await execResp.text());
712
634
  }
@@ -730,7 +652,7 @@ class OneShot {
730
652
  this.validate(name, 'name');
731
653
  const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
732
654
  method: 'POST',
733
- headers: { 'Content-Type': 'application/json', ...this.headers() },
655
+ headers: this.jsonHeaders(),
734
656
  body: JSON.stringify({ name }),
735
657
  });
736
658
  if (!response.ok) {
@@ -811,14 +733,11 @@ class OneShot {
811
733
  * ```
812
734
  */
813
735
  async smsInboxList(options = {}) {
814
- const params = new URLSearchParams();
815
- if (options.since)
816
- params.set('since', options.since);
817
- if (options.limit)
818
- params.set('limit', String(options.limit));
819
- if (options.from)
820
- params.set('from', options.from);
821
- 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
+ });
822
741
  const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
823
742
  headers: this.headers()
824
743
  });
@@ -862,12 +781,10 @@ class OneShot {
862
781
  * ```
863
782
  */
864
783
  async notifications(options = {}) {
865
- const params = new URLSearchParams();
866
- if (options.unread)
867
- params.set('unread', 'true');
868
- if (options.limit)
869
- params.set('limit', String(options.limit));
870
- const qs = params.toString();
784
+ const qs = this.buildQuery({
785
+ unread: options.unread ? 'true' : undefined,
786
+ limit: options.limit || undefined,
787
+ });
871
788
  const response = await fetch(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
872
789
  headers: this.headers()
873
790
  });
@@ -959,36 +876,24 @@ class OneShot {
959
876
  payload.soul_service_slug = options.soul_service_slug;
960
877
  if (options.schedule)
961
878
  payload.schedule = options.schedule;
962
- // First call: get quote (402)
963
- const quoteResp = await this.makeRequest('/v1/compute', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
964
- if (quoteResp.status === 400) {
965
- const errorData = await quoteResp.json();
966
- if (errorData.error === 'content_blocked') {
967
- throw new errors_1.ContentBlockedError(errorData.message, []);
968
- }
969
- throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
970
- }
971
- if (quoteResp.status !== 402) {
972
- throw new errors_1.ToolError('Expected 402 for compute quote', quoteResp.status, await quoteResp.text());
973
- }
974
- const quoteData = await quoteResp.json();
975
- this.log(`Compute quote: $${quoteData.context.total_budget} — ${quoteData.context.objective_summary}`);
976
- if (options.maxCost && parseFloat(quoteData.context.total_budget) > options.maxCost) {
977
- throw new errors_1.OneShotError(`Quote $${quoteData.context.total_budget} exceeds maxCost $${options.maxCost}`);
978
- }
979
- const paymentInfo = {
980
- protocol: 'x402',
981
- network: `eip155:${quoteData.payment_request.chain_id}`,
982
- payTo: quoteData.payment_request.recipient,
983
- amount: quoteData.payment_request.amount,
984
- currency: 'USD',
985
- facilitator_url: this.baseUrl,
986
- token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
987
- };
988
- this.checkAbortBeforePayment(options.signal);
989
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, '/v1/compute', payload, quoteData.context.quote_id, options.signal);
990
- const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
991
- 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
+ });
992
897
  if (createResp.status !== 202) {
993
898
  throw new errors_1.ToolError('Compute goal creation failed', createResp.status, await createResp.text());
994
899
  }
@@ -1075,7 +980,7 @@ class OneShot {
1075
980
  this.validate(goalId, 'goalId');
1076
981
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1077
982
  method: 'POST',
1078
- headers: { 'Content-Type': 'application/json', ...this.headers() },
983
+ headers: this.jsonHeaders(),
1079
984
  body: JSON.stringify({ reason })
1080
985
  });
1081
986
  if (response.status === 404) {
@@ -1104,7 +1009,7 @@ class OneShot {
1104
1009
  this.validate(input.task_id, 'task_id');
1105
1010
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1106
1011
  method: 'POST',
1107
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1012
+ headers: this.jsonHeaders(),
1108
1013
  body: JSON.stringify(input)
1109
1014
  });
1110
1015
  if (response.status === 404) {
@@ -1128,7 +1033,7 @@ class OneShot {
1128
1033
  this.validate(goalId, 'goalId');
1129
1034
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1130
1035
  method: 'POST',
1131
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1036
+ headers: this.jsonHeaders(),
1132
1037
  body: JSON.stringify({ reason })
1133
1038
  });
1134
1039
  if (!response.ok) {
@@ -1150,7 +1055,7 @@ class OneShot {
1150
1055
  this.validate(goalId, 'goalId');
1151
1056
  const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1152
1057
  method: 'POST',
1153
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1058
+ headers: this.jsonHeaders(),
1154
1059
  body: JSON.stringify({})
1155
1060
  });
1156
1061
  if (!response.ok) {
@@ -1219,10 +1124,7 @@ class OneShot {
1219
1124
  * ```
1220
1125
  */
1221
1126
  async spendBreakdown(options) {
1222
- const params = new URLSearchParams();
1223
- if (options?.period)
1224
- params.set('period', String(options.period));
1225
- const qs = params.toString();
1127
+ const qs = this.buildQuery({ period: options?.period || undefined });
1226
1128
  const response = await fetch(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
1227
1129
  headers: this.headers()
1228
1130
  });
@@ -1241,10 +1143,7 @@ class OneShot {
1241
1143
  * ```
1242
1144
  */
1243
1145
  async rocs(options) {
1244
- const params = new URLSearchParams();
1245
- if (options?.period)
1246
- params.set('period', String(options.period));
1247
- const qs = params.toString();
1146
+ const qs = this.buildQuery({ period: options?.period || undefined });
1248
1147
  const response = await fetch(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
1249
1148
  headers: this.headers()
1250
1149
  });
@@ -1265,14 +1164,11 @@ class OneShot {
1265
1164
  * ```
1266
1165
  */
1267
1166
  async receiptsList(options) {
1268
- const params = new URLSearchParams();
1269
- if (options?.period)
1270
- params.set('period', String(options.period));
1271
- if (options?.category)
1272
- params.set('category', options.category);
1273
- if (options?.limit)
1274
- params.set('limit', String(options.limit));
1275
- 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
+ });
1276
1172
  const response = await fetch(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
1277
1173
  headers: this.headers()
1278
1174
  });
@@ -1324,6 +1220,26 @@ class OneShot {
1324
1220
  'X-OneShot-SDK-Version': SDK_VERSION
1325
1221
  };
1326
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
+ }
1327
1243
  /**
1328
1244
  * Header that asks the API to reject the request when the computed quote
1329
1245
  * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
@@ -1416,15 +1332,55 @@ class OneShot {
1416
1332
  }
1417
1333
  return (result.data ?? result);
1418
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
+ }
1419
1368
  async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts) {
1420
- // 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;
1421
1374
  let result;
1422
1375
  try {
1423
1376
  result = await this.waitViaWebSocket(requestId, timeoutSec, signal, onStatusUpdate);
1424
1377
  }
1425
1378
  catch {
1426
1379
  this.log('WebSocket unavailable, falling back to HTTP polling');
1427
- 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);
1428
1384
  }
1429
1385
  // Optional second phase: keep polling for the async phone-reveal webhook.
1430
1386
  // Only kicks in when the caller explicitly opts in AND the result still
@@ -1601,7 +1557,7 @@ class OneShot {
1601
1557
  else if (msg.status === 'failed') {
1602
1558
  settle(() => {
1603
1559
  cleanup();
1604
- 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));
1605
1561
  });
1606
1562
  }
1607
1563
  else {
@@ -1657,7 +1613,7 @@ class OneShot {
1657
1613
  return result;
1658
1614
  }
1659
1615
  if (job.status === 'failed') {
1660
- 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);
1661
1617
  }
1662
1618
  onStatusUpdate?.(job.status, requestId);
1663
1619
  retries = 0;