@oneshot-agent/sdk 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/errors.d.ts +64 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +63 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +80 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +251 -12
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
|
|
|
49
49
|
Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
|
|
50
50
|
__exportStar(require("./errors"), exports);
|
|
51
51
|
// Keep in sync with package.json `version`. Guarded by version.test.ts.
|
|
52
|
-
const SDK_VERSION = '0.
|
|
52
|
+
const SDK_VERSION = '0.27.0';
|
|
53
53
|
// ============================================================================
|
|
54
54
|
// Environment Configuration
|
|
55
55
|
// ============================================================================
|
|
@@ -76,6 +76,39 @@ __exportStar(require("./types"), exports);
|
|
|
76
76
|
* await agent.email({ to: 'user@example.com', subject: 'Hi', body: 'Hello' });
|
|
77
77
|
* ```
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* Reject a budget config the server would reject, at construction rather than
|
|
81
|
+
* on the first paid call — a typo'd cap must not become "no cap".
|
|
82
|
+
*/
|
|
83
|
+
function validateBudgetConfig(budgets) {
|
|
84
|
+
if (!budgets)
|
|
85
|
+
return undefined;
|
|
86
|
+
// A typo'd key (`daliy`) from an untyped caller must not silently mean "no cap".
|
|
87
|
+
for (const key of Object.keys(budgets)) {
|
|
88
|
+
if (!['daily', 'perTransaction', 'alertAt', 'pauseAt'].includes(key)) {
|
|
89
|
+
throw new errors_1.ValidationError(`budgets.${key} is not a recognized field`, `budgets.${key}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const positive = (v, field) => {
|
|
93
|
+
if (v === undefined)
|
|
94
|
+
return;
|
|
95
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
96
|
+
throw new errors_1.ValidationError(`budgets.${field} must be a positive number`, `budgets.${field}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const fraction = (v, field) => {
|
|
100
|
+
if (v === undefined)
|
|
101
|
+
return;
|
|
102
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0 || v > 1) {
|
|
103
|
+
throw new errors_1.ValidationError(`budgets.${field} must be a fraction in (0, 1]`, `budgets.${field}`);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
positive(budgets.daily, 'daily');
|
|
107
|
+
positive(budgets.perTransaction, 'perTransaction');
|
|
108
|
+
fraction(budgets.alertAt, 'alertAt');
|
|
109
|
+
fraction(budgets.pauseAt, 'pauseAt');
|
|
110
|
+
return budgets;
|
|
111
|
+
}
|
|
79
112
|
class OneShot {
|
|
80
113
|
/**
|
|
81
114
|
* Async factory — required for CDP wallets (account creation is async).
|
|
@@ -117,6 +150,8 @@ class OneShot {
|
|
|
117
150
|
this.logger = config.logger ?? console.log;
|
|
118
151
|
this._currency = config.currency ?? 'USDC';
|
|
119
152
|
this._slippage = config.slippage ?? 0.01;
|
|
153
|
+
this._budgets = validateBudgetConfig(config.budgets);
|
|
154
|
+
this._alertEmail = config.alerts?.email;
|
|
120
155
|
this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
|
|
121
156
|
if (walletProvider) {
|
|
122
157
|
this.provider = walletProvider;
|
|
@@ -153,6 +188,10 @@ class OneShot {
|
|
|
153
188
|
get slippage() {
|
|
154
189
|
return this._slippage;
|
|
155
190
|
}
|
|
191
|
+
/** The budget config this instance was constructed with, if any. */
|
|
192
|
+
get budgetConfig() {
|
|
193
|
+
return this._budgets;
|
|
194
|
+
}
|
|
156
195
|
// ---------------------------------------------------------------------------
|
|
157
196
|
// Public methods
|
|
158
197
|
// ---------------------------------------------------------------------------
|
|
@@ -375,7 +414,7 @@ class OneShot {
|
|
|
375
414
|
onQuote: (ctx) => this.log(`Commerce quote: $${ctx.total} for "${ctx.product_title}"`),
|
|
376
415
|
});
|
|
377
416
|
if (buyResp.status !== 202) {
|
|
378
|
-
|
|
417
|
+
await this.failFromResponse('Commerce buy failed', buyResp);
|
|
379
418
|
}
|
|
380
419
|
const result = await buyResp.json();
|
|
381
420
|
this.log(`Order submitted: ${result.request_id}`);
|
|
@@ -452,7 +491,7 @@ class OneShot {
|
|
|
452
491
|
},
|
|
453
492
|
});
|
|
454
493
|
if (callResp.status !== 202) {
|
|
455
|
-
|
|
494
|
+
await this.failFromResponse('Voice call initiation failed', callResp);
|
|
456
495
|
}
|
|
457
496
|
const result = await callResp.json();
|
|
458
497
|
this.log(`Call initiated: ${result.request_id}`);
|
|
@@ -515,7 +554,7 @@ class OneShot {
|
|
|
515
554
|
},
|
|
516
555
|
});
|
|
517
556
|
if (sendResp.status !== 202) {
|
|
518
|
-
|
|
557
|
+
await this.failFromResponse('SMS send failed', sendResp);
|
|
519
558
|
}
|
|
520
559
|
const result = await sendResp.json();
|
|
521
560
|
this.log(`SMS queued: ${result.request_id}`);
|
|
@@ -584,7 +623,7 @@ class OneShot {
|
|
|
584
623
|
},
|
|
585
624
|
});
|
|
586
625
|
if (buildResp.status !== 202) {
|
|
587
|
-
|
|
626
|
+
await this.failFromResponse('Build initiation failed', buildResp);
|
|
588
627
|
}
|
|
589
628
|
const result = await buildResp.json();
|
|
590
629
|
this.log(`Build initiated: ${result.request_id}`);
|
|
@@ -646,7 +685,7 @@ class OneShot {
|
|
|
646
685
|
},
|
|
647
686
|
});
|
|
648
687
|
if (execResp.status !== 202) {
|
|
649
|
-
|
|
688
|
+
await this.failFromResponse('Browser task initiation failed', execResp);
|
|
650
689
|
}
|
|
651
690
|
const result = await execResp.json();
|
|
652
691
|
this.log(`Browser task initiated: ${result.request_id}`);
|
|
@@ -911,7 +950,7 @@ class OneShot {
|
|
|
911
950
|
},
|
|
912
951
|
});
|
|
913
952
|
if (createResp.status !== 202) {
|
|
914
|
-
|
|
953
|
+
await this.failFromResponse('Compute goal creation failed', createResp);
|
|
915
954
|
}
|
|
916
955
|
return createResp.json();
|
|
917
956
|
}
|
|
@@ -1099,13 +1138,16 @@ class OneShot {
|
|
|
1099
1138
|
}
|
|
1100
1139
|
const path = `/v1/compute/${goalId}/fund`;
|
|
1101
1140
|
const payload = { amount };
|
|
1141
|
+
await this.ensureBudgetsSynced();
|
|
1102
1142
|
// First call: get quote (402)
|
|
1103
1143
|
const quoteResp = await this.makeRequest(path, payload);
|
|
1104
1144
|
if (quoteResp.status !== 402) {
|
|
1105
|
-
|
|
1145
|
+
// A 403 here is the budget gate on the quote leg → BudgetExceededError.
|
|
1146
|
+
await this.failFromResponse('Expected 402 for compute fund quote', quoteResp);
|
|
1106
1147
|
}
|
|
1107
1148
|
const quoteData = await quoteResp.json();
|
|
1108
1149
|
this.log(`Compute fund quote: $${quoteData.payment_request.amount} to top up ${goalId}`);
|
|
1150
|
+
this.assertWithinBudget(quoteData.payment_request.amount);
|
|
1109
1151
|
const paymentInfo = {
|
|
1110
1152
|
protocol: 'x402',
|
|
1111
1153
|
network: `eip155:${quoteData.payment_request.chain_id}`,
|
|
@@ -1116,10 +1158,11 @@ class OneShot {
|
|
|
1116
1158
|
token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
|
|
1117
1159
|
};
|
|
1118
1160
|
const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, path, payload, quoteData.context.quote_id);
|
|
1161
|
+
paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
|
|
1119
1162
|
const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1120
1163
|
const fundResp = await this.makeRequest(path, payload, auth, quoteData.context.quote_id);
|
|
1121
1164
|
if (!fundResp.ok) {
|
|
1122
|
-
|
|
1165
|
+
await this.failFromResponse('Failed to fund compute goal', fundResp);
|
|
1123
1166
|
}
|
|
1124
1167
|
const json = await fundResp.json();
|
|
1125
1168
|
return json.data;
|
|
@@ -1347,6 +1390,100 @@ class OneShot {
|
|
|
1347
1390
|
}
|
|
1348
1391
|
return qs.toString();
|
|
1349
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Push `config.budgets` to the server once, before the first paid call.
|
|
1395
|
+
*
|
|
1396
|
+
* Lazy rather than in the constructor because the PUT is signed (async) and
|
|
1397
|
+
* the constructor is sync. Idempotent per instance via the cached promise,
|
|
1398
|
+
* which is only kept once the sync SUCCEEDS.
|
|
1399
|
+
*
|
|
1400
|
+
* FAILS CLOSED. If the server can't confirm the budget — network error,
|
|
1401
|
+
* 5xx, 429, or a rejected config — this throws BudgetSyncError and the paid
|
|
1402
|
+
* call is not made. Proceeding would silently drop the guardrail the
|
|
1403
|
+
* developer configured, which is exactly the "empty wallet at 3am" the
|
|
1404
|
+
* budget exists to prevent. The marker is cleared so the next paid call
|
|
1405
|
+
* retries.
|
|
1406
|
+
*/
|
|
1407
|
+
async ensureBudgetsSynced() {
|
|
1408
|
+
if (!this._budgets && !this._alertEmail)
|
|
1409
|
+
return;
|
|
1410
|
+
if (this._budgetSync)
|
|
1411
|
+
return this._budgetSync;
|
|
1412
|
+
const attempt = (async () => {
|
|
1413
|
+
const body = {};
|
|
1414
|
+
if (this._budgets?.daily !== undefined)
|
|
1415
|
+
body.daily = this._budgets.daily;
|
|
1416
|
+
if (this._budgets?.perTransaction !== undefined)
|
|
1417
|
+
body.per_transaction = this._budgets.perTransaction;
|
|
1418
|
+
if (this._budgets?.alertAt !== undefined)
|
|
1419
|
+
body.alert_at = this._budgets.alertAt;
|
|
1420
|
+
if (this._budgets?.pauseAt !== undefined)
|
|
1421
|
+
body.pause_at = this._budgets.pauseAt;
|
|
1422
|
+
if (this._alertEmail !== undefined)
|
|
1423
|
+
body.alert_email = this._alertEmail;
|
|
1424
|
+
let response;
|
|
1425
|
+
try {
|
|
1426
|
+
response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
|
|
1427
|
+
method: 'PUT',
|
|
1428
|
+
headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
|
|
1429
|
+
body: JSON.stringify(body),
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
catch (err) {
|
|
1433
|
+
throw new errors_1.BudgetSyncError(`Could not sync spend budget (network): ${err}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (!response.ok) {
|
|
1436
|
+
const text = await response.text();
|
|
1437
|
+
throw new errors_1.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text);
|
|
1438
|
+
}
|
|
1439
|
+
this.log('Budget synced');
|
|
1440
|
+
})();
|
|
1441
|
+
this._budgetSync = attempt;
|
|
1442
|
+
try {
|
|
1443
|
+
await attempt;
|
|
1444
|
+
}
|
|
1445
|
+
catch (err) {
|
|
1446
|
+
// Not synced: clear the marker so the next paid call retries, then
|
|
1447
|
+
// refuse this one rather than run it unguarded.
|
|
1448
|
+
this._budgetSync = undefined;
|
|
1449
|
+
this.log(`${err.message} — paid call refused until the budget is confirmed`);
|
|
1450
|
+
throw err;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Local fast-fail on the per-transaction cap, so an oversized call fails
|
|
1455
|
+
* without a network round-trip. The server enforces the same cap (and the
|
|
1456
|
+
* daily one, which needs the ledger) regardless of which client calls —
|
|
1457
|
+
* this is the same SDK-checks/server-enforces split as maxCost.
|
|
1458
|
+
*/
|
|
1459
|
+
assertWithinBudget(total) {
|
|
1460
|
+
const cap = this._budgets?.perTransaction;
|
|
1461
|
+
if (!cap || cap <= 0)
|
|
1462
|
+
return;
|
|
1463
|
+
const amount = parseFloat(total);
|
|
1464
|
+
if (Number.isFinite(amount) && amount > cap) {
|
|
1465
|
+
throw new errors_1.BudgetExceededError(`Quote $${total} exceeds this agent's per-transaction budget of $${cap}`, 'per_transaction', cap, undefined, amount);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Current spend budget and today's utilization.
|
|
1470
|
+
*
|
|
1471
|
+
* @example
|
|
1472
|
+
* ```typescript
|
|
1473
|
+
* const b = await agent.budgets();
|
|
1474
|
+
* console.log(`${b.spent_today_usdc} of ${b.daily_usdc} spent, resets ${b.resets_at}`);
|
|
1475
|
+
* ```
|
|
1476
|
+
*/
|
|
1477
|
+
async budgets() {
|
|
1478
|
+
const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
|
|
1479
|
+
headers: await this.signedReadHeaders(),
|
|
1480
|
+
});
|
|
1481
|
+
if (!response.ok) {
|
|
1482
|
+
throw new errors_1.ToolError('Failed to fetch budgets', response.status, await response.text());
|
|
1483
|
+
}
|
|
1484
|
+
const body = await response.json();
|
|
1485
|
+
return (body.data ?? body);
|
|
1486
|
+
}
|
|
1350
1487
|
/** Local fast-fail guard: throw when a quote total exceeds the caller's cap. */
|
|
1351
1488
|
assertWithinMaxCost(total, maxCost) {
|
|
1352
1489
|
if (maxCost && parseFloat(total) > maxCost) {
|
|
@@ -1360,6 +1497,99 @@ class OneShot {
|
|
|
1360
1497
|
* a server-side enforcement layer so non-SDK callers (MCP server, custom
|
|
1361
1498
|
* integrations) can't ignore the cap.
|
|
1362
1499
|
*/
|
|
1500
|
+
/**
|
|
1501
|
+
* The amount to sign, in decimal USDC.
|
|
1502
|
+
*
|
|
1503
|
+
* A 402 advertises its price twice: the x402 v2 `PAYMENT-REQUIRED` header
|
|
1504
|
+
* (`accepts[0].amount`, atomic units) and the legacy JSON body
|
|
1505
|
+
* (`payment_request.amount`, decimal). The header is authoritative — it is
|
|
1506
|
+
* what the server rebuilds its requirement from and what
|
|
1507
|
+
* `findMatchingRequirements` compares the signature against.
|
|
1508
|
+
*
|
|
1509
|
+
* Preferring the body is how quote-based routes (email/send, sms, voice,
|
|
1510
|
+
* build, commerce/buy, compute) silently failed: the body carried a
|
|
1511
|
+
* hardcoded "0.00", so the SDK signed a zero-cost authorization against a
|
|
1512
|
+
* real charge and the server answered with a bodiless 402. It stayed hidden
|
|
1513
|
+
* for as long as credits covered those calls, since the credit path returns
|
|
1514
|
+
* 202 without any payment handshake.
|
|
1515
|
+
*
|
|
1516
|
+
* Falls back to the body when the header is missing or unparseable.
|
|
1517
|
+
*/
|
|
1518
|
+
/**
|
|
1519
|
+
* Throw the most specific error a failed response supports.
|
|
1520
|
+
*
|
|
1521
|
+
* A 402 arriving on the PAID retry means the facilitator refused the
|
|
1522
|
+
* signature — not the ordinary "here is your quote" 402. The API names the
|
|
1523
|
+
* cause in that body (`payment_verification_failed` + reason + expected vs
|
|
1524
|
+
* received amount); surface it as a `PaymentError` so callers can branch on
|
|
1525
|
+
* `err.reason` instead of string-matching. Anything else keeps the existing
|
|
1526
|
+
* `ToolError` shape.
|
|
1527
|
+
*/
|
|
1528
|
+
async failFromResponse(message, response) {
|
|
1529
|
+
const text = await response.text();
|
|
1530
|
+
const rejection = response.status === 402 ? this.parsePaymentRejection(text) : undefined;
|
|
1531
|
+
if (rejection)
|
|
1532
|
+
throw rejection;
|
|
1533
|
+
const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
|
|
1534
|
+
if (budget)
|
|
1535
|
+
throw budget;
|
|
1536
|
+
throw new errors_1.ToolError(message, response.status, text);
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Map a 403 `budget_exceeded` body onto a typed error, so callers can catch
|
|
1540
|
+
* "my own budget stopped this" separately from an auth failure or a payment
|
|
1541
|
+
* rejection. Any other 403 falls through to ToolError.
|
|
1542
|
+
*/
|
|
1543
|
+
parseBudgetRejection(text) {
|
|
1544
|
+
try {
|
|
1545
|
+
const body = JSON.parse(text);
|
|
1546
|
+
if (body.error !== 'budget_exceeded')
|
|
1547
|
+
return undefined;
|
|
1548
|
+
const b = body.budget ?? {};
|
|
1549
|
+
return new errors_1.BudgetExceededError(body.message ?? 'Agent spend budget exceeded', b.reason === 'per_transaction' ? 'per_transaction' : 'daily', b.cap !== undefined ? parseFloat(b.cap) : undefined, b.spent !== undefined ? parseFloat(b.spent) : undefined, b.charge !== undefined ? parseFloat(b.charge) : undefined, b.resets_at);
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
parsePaymentRejection(text) {
|
|
1556
|
+
let data;
|
|
1557
|
+
try {
|
|
1558
|
+
data = JSON.parse(text);
|
|
1559
|
+
}
|
|
1560
|
+
catch {
|
|
1561
|
+
return undefined;
|
|
1562
|
+
}
|
|
1563
|
+
if (data?.error !== 'payment_verification_failed')
|
|
1564
|
+
return undefined;
|
|
1565
|
+
const reason = data.reason || 'unknown';
|
|
1566
|
+
const expectedAmount = data.expected?.amount;
|
|
1567
|
+
const receivedAmount = data.received?.amount;
|
|
1568
|
+
const detail = [
|
|
1569
|
+
expectedAmount ? `expected $${expectedAmount}` : null,
|
|
1570
|
+
receivedAmount ? `signed $${receivedAmount}` : null,
|
|
1571
|
+
].filter(Boolean).join(', ');
|
|
1572
|
+
return new errors_1.PaymentError(`payment rejected: ${reason}${detail ? ` — ${detail}` : ''}${data.message ? ` (${data.message})` : ''}`, reason, {
|
|
1573
|
+
amount: expectedAmount,
|
|
1574
|
+
asset: data.expected?.asset,
|
|
1575
|
+
network: data.expected?.network,
|
|
1576
|
+
payTo: data.expected?.pay_to,
|
|
1577
|
+
}, { amount: receivedAmount }, data.quote_id);
|
|
1578
|
+
}
|
|
1579
|
+
chargeAmount(accepted, bodyAmount) {
|
|
1580
|
+
if (accepted?.amount == null || !/^\d+$/.test(String(accepted.amount))) {
|
|
1581
|
+
return bodyAmount ?? '0';
|
|
1582
|
+
}
|
|
1583
|
+
const fromHeader = ethers_1.ethers.formatUnits(accepted.amount, 6);
|
|
1584
|
+
// When the two agree (every fixed-price route), keep the body's string
|
|
1585
|
+
// verbatim — downstream consumers such as the ETH auto-swap pass this
|
|
1586
|
+
// value along, and there is no reason to reformat it. Only a genuine
|
|
1587
|
+
// disagreement flips to the header.
|
|
1588
|
+
if (bodyAmount != null && parseFloat(bodyAmount) === parseFloat(fromHeader)) {
|
|
1589
|
+
return bodyAmount;
|
|
1590
|
+
}
|
|
1591
|
+
return fromHeader;
|
|
1592
|
+
}
|
|
1363
1593
|
maxCostHeader(maxCost) {
|
|
1364
1594
|
if (!maxCost || maxCost <= 0)
|
|
1365
1595
|
return undefined;
|
|
@@ -1409,6 +1639,9 @@ class OneShot {
|
|
|
1409
1639
|
if (signal?.aborted) {
|
|
1410
1640
|
throw new errors_1.OneShotError('Operation cancelled');
|
|
1411
1641
|
}
|
|
1642
|
+
// One-time push of config.budgets before the first paid call, so the
|
|
1643
|
+
// server-side gate knows about them on this very request.
|
|
1644
|
+
await this.ensureBudgetsSynced();
|
|
1412
1645
|
let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
|
|
1413
1646
|
// Handle 402 Payment Required
|
|
1414
1647
|
if (response.status === 402) {
|
|
@@ -1421,18 +1654,19 @@ class OneShot {
|
|
|
1421
1654
|
protocol: 'x402',
|
|
1422
1655
|
network: accepted.network,
|
|
1423
1656
|
payTo: accepted.payTo,
|
|
1424
|
-
amount: data.payment_request?.amount
|
|
1657
|
+
amount: this.chargeAmount(accepted, data.payment_request?.amount),
|
|
1425
1658
|
currency: 'USD',
|
|
1426
1659
|
facilitator_url: this.baseUrl,
|
|
1427
1660
|
token: { address: accepted.asset, symbol: 'USDC', decimals: 6 }
|
|
1428
1661
|
};
|
|
1429
1662
|
this.log(`Payment required: ${paymentInfo.amount} USDC`);
|
|
1663
|
+
this.assertWithinBudget(paymentInfo.amount);
|
|
1430
1664
|
this.checkAbortBeforePayment(signal);
|
|
1431
1665
|
const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1432
1666
|
response = await this.makeRequest(endpoint, payload, auth, quoteId, signal, undefined, extraHeaders);
|
|
1433
1667
|
}
|
|
1434
1668
|
if (!response.ok) {
|
|
1435
|
-
|
|
1669
|
+
await this.failFromResponse('Tool request failed', response);
|
|
1436
1670
|
}
|
|
1437
1671
|
const result = await response.json();
|
|
1438
1672
|
// Handle async jobs
|
|
@@ -1453,16 +1687,20 @@ class OneShot {
|
|
|
1453
1687
|
* body directly), since that differs per tool.
|
|
1454
1688
|
*/
|
|
1455
1689
|
async runQuoteToPay(cfg) {
|
|
1690
|
+
await this.ensureBudgetsSynced();
|
|
1456
1691
|
const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, this.maxCostHeader(cfg.maxCost));
|
|
1457
1692
|
if (quoteResp.status === 400 && cfg.on400) {
|
|
1458
1693
|
await cfg.on400(quoteResp);
|
|
1459
1694
|
}
|
|
1460
1695
|
if (quoteResp.status !== 402) {
|
|
1461
|
-
|
|
1696
|
+
// Routes the budget gate on the quote leg (fixed-price ones) surface a
|
|
1697
|
+
// 403 here; failFromResponse maps it to BudgetExceededError.
|
|
1698
|
+
await this.failFromResponse(cfg.expectMsg, quoteResp);
|
|
1462
1699
|
}
|
|
1463
1700
|
const quoteData = await quoteResp.json();
|
|
1464
1701
|
cfg.onQuote(quoteData.context);
|
|
1465
1702
|
this.assertWithinMaxCost(cfg.totalOf(quoteData.context), cfg.maxCost);
|
|
1703
|
+
this.assertWithinBudget(cfg.totalOf(quoteData.context));
|
|
1466
1704
|
const paymentInfo = {
|
|
1467
1705
|
protocol: 'x402',
|
|
1468
1706
|
network: `eip155:${quoteData.payment_request.chain_id}`,
|
|
@@ -1474,6 +1712,7 @@ class OneShot {
|
|
|
1474
1712
|
};
|
|
1475
1713
|
this.checkAbortBeforePayment(cfg.signal);
|
|
1476
1714
|
const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal);
|
|
1715
|
+
paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
|
|
1477
1716
|
const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1478
1717
|
const execResp = await this.makeRequest(cfg.endpoint, cfg.payload, auth, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
|
|
1479
1718
|
return { context: quoteData.context, execResp };
|