@piprail/sdk 2.8.0 → 2.10.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +7 -4
  3. package/dist/{algorand-WJL6VRXW.js → algorand-AA3WXKW4.js} +1 -1
  4. package/dist/{algorand-EKD34T4Q.cjs → algorand-FCEECDG6.cjs} +32 -32
  5. package/dist/{aptos-CVHLVMAF.cjs → aptos-GHJPO6JJ.cjs} +31 -31
  6. package/dist/{aptos-WMPFEASY.js → aptos-LY67Q6QF.js} +1 -1
  7. package/dist/chunk-3SBTJKAG.js +92 -0
  8. package/dist/chunk-CQQI5IJX.cjs +92 -0
  9. package/dist/{chunk-VPENSVOI.cjs → chunk-MWBT7MCE.cjs} +7 -3
  10. package/dist/chunk-O4UQOZ4Z.cjs +14 -0
  11. package/dist/{chunk-W3HOMXOF.js → chunk-SC2ZYDHD.js} +6 -2
  12. package/dist/chunk-SK3CB7UA.js +14 -0
  13. package/dist/index.cjs +2247 -425
  14. package/dist/index.d.cts +3094 -2583
  15. package/dist/index.d.ts +3094 -2583
  16. package/dist/index.js +1937 -115
  17. package/dist/ledger-OQos-tmj.d.cts +925 -0
  18. package/dist/ledger-OQos-tmj.d.ts +925 -0
  19. package/dist/{near-NYFMX2KA.cjs → near-6KAQVVG2.cjs} +26 -26
  20. package/dist/{near-QV4IOZNX.js → near-FZBUICCS.js} +1 -1
  21. package/dist/node.cjs +38 -0
  22. package/dist/node.d.cts +16 -0
  23. package/dist/node.d.ts +16 -0
  24. package/dist/node.js +38 -0
  25. package/dist/receipt-NNEID77X.js +14 -0
  26. package/dist/receipt-QCDPMWNF.cjs +14 -0
  27. package/dist/{solana-Z34OWXMO.js → solana-ELUWO6N5.js} +1 -1
  28. package/dist/{solana-L342QCSV.cjs → solana-MYF4HBO4.cjs} +32 -32
  29. package/dist/{stellar-PWC4LETJ.js → stellar-ASP2THL2.js} +2 -2
  30. package/dist/{stellar-YEXRVR3W.cjs → stellar-CRWBSX4E.cjs} +21 -21
  31. package/dist/{sui-VEOGHBYE.js → sui-FZIKZNVI.js} +14 -4
  32. package/dist/{sui-EFRSCDWN.cjs → sui-Y4RLKKE2.cjs} +29 -19
  33. package/dist/{ton-N7KSPSAR.js → ton-7GKCTC5H.js} +1 -1
  34. package/dist/{ton-LZKJWYHV.cjs → ton-AOR3EURW.cjs} +16 -16
  35. package/dist/{tron-WKDUQVZR.js → tron-BMCWN5SS.js} +8 -2
  36. package/dist/{tron-DQU4WTGQ.cjs → tron-OMXB6EW2.cjs} +31 -25
  37. package/dist/{xrpl-6B4IYRWI.js → xrpl-DD7TJL5L.js} +1 -1
  38. package/dist/{xrpl-K3U3SBF4.cjs → xrpl-Y6SQNYLC.cjs} +20 -20
  39. package/package.json +11 -1
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
+ import {
2
+ memorySpendStore
3
+ } from "./chunk-SK3CB7UA.js";
4
+ import {
5
+ signReceiptEvm
6
+ } from "./chunk-3SBTJKAG.js";
1
7
  import {
2
8
  ConfirmationTimeoutError,
3
9
  InsufficientFundsError,
4
10
  InvalidEnvelopeError,
11
+ MAX_DECIMALS,
5
12
  MaxRetriesExceededError,
6
13
  MissingDriverError,
7
14
  NoCompatibleAcceptError,
@@ -24,7 +31,7 @@ import {
24
31
  parseUnits,
25
32
  rejectForeignToken,
26
33
  toInsufficientFundsError
27
- } from "./chunk-W3HOMXOF.js";
34
+ } from "./chunk-SC2ZYDHD.js";
28
35
 
29
36
  // src/drivers/registry.ts
30
37
  var byFamily = /* @__PURE__ */ new Map();
@@ -67,7 +74,7 @@ function resolveNetwork(opts) {
67
74
  }
68
75
 
69
76
  // src/drivers/evm/index.ts
70
- import { BaseError, createPublicClient, erc20Abi as erc20Abi4, getAddress as getAddress4, http as http2, isAddress } from "viem";
77
+ import { BaseError, createPublicClient, erc20Abi as erc20Abi4, getAddress as getAddress5, http as http2, isAddress } from "viem";
71
78
 
72
79
  // src/drivers/evm/chains.ts
73
80
  import { defineChain } from "viem";
@@ -1353,6 +1360,361 @@ async function verifyAndSettlePermit2Evm(input) {
1353
1360
  };
1354
1361
  }
1355
1362
 
1363
+ // src/drivers/evm/upto.ts
1364
+ import {
1365
+ getAddress as getAddress4,
1366
+ recoverTypedDataAddress as recoverTypedDataAddress3
1367
+ } from "viem";
1368
+ var X402_UPTO_PERMIT2_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
1369
+ var UPTO_PROXY_CHAIN_IDS = /* @__PURE__ */ new Set([
1370
+ 1,
1371
+ // Ethereum
1372
+ 8453,
1373
+ // Base
1374
+ 42161,
1375
+ // Arbitrum
1376
+ 10,
1377
+ // Optimism
1378
+ 137,
1379
+ // Polygon
1380
+ 56
1381
+ // BNB
1382
+ ]);
1383
+ function isUptoProxyChain(chainId) {
1384
+ return UPTO_PROXY_CHAIN_IDS.has(chainId);
1385
+ }
1386
+ var PERMIT2_UPTO_WITNESS_TYPES = {
1387
+ PermitWitnessTransferFrom: PERMIT2_WITNESS_TYPES.PermitWitnessTransferFrom,
1388
+ TokenPermissions: PERMIT2_WITNESS_TYPES.TokenPermissions,
1389
+ Witness: [
1390
+ { name: "to", type: "address" },
1391
+ { name: "facilitator", type: "address" },
1392
+ { name: "validAfter", type: "uint256" }
1393
+ ]
1394
+ };
1395
+ var x402UptoProxyAbi = [
1396
+ {
1397
+ type: "function",
1398
+ name: "settle",
1399
+ stateMutability: "nonpayable",
1400
+ outputs: [],
1401
+ inputs: [
1402
+ {
1403
+ name: "permit",
1404
+ type: "tuple",
1405
+ components: [
1406
+ {
1407
+ name: "permitted",
1408
+ type: "tuple",
1409
+ components: [
1410
+ { name: "token", type: "address" },
1411
+ { name: "amount", type: "uint256" }
1412
+ ]
1413
+ },
1414
+ { name: "nonce", type: "uint256" },
1415
+ { name: "deadline", type: "uint256" }
1416
+ ]
1417
+ },
1418
+ { name: "amount", type: "uint256" },
1419
+ { name: "owner", type: "address" },
1420
+ {
1421
+ name: "witness",
1422
+ type: "tuple",
1423
+ components: [
1424
+ { name: "to", type: "address" },
1425
+ { name: "facilitator", type: "address" },
1426
+ { name: "validAfter", type: "uint256" }
1427
+ ]
1428
+ },
1429
+ { name: "signature", type: "bytes" }
1430
+ ]
1431
+ }
1432
+ ];
1433
+ function shorten3(msg) {
1434
+ const oneLine = msg.replace(/\s+/g, " ").trim();
1435
+ return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
1436
+ }
1437
+ function randomPermit2Nonce2() {
1438
+ const g = globalThis.crypto;
1439
+ if (!g?.getRandomValues) {
1440
+ throw new UnsupportedSchemeError(
1441
+ "this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the upto rail needs a CSPRNG nonce."
1442
+ );
1443
+ }
1444
+ const raw = new Uint8Array(32);
1445
+ g.getRandomValues(raw);
1446
+ return BigInt(`0x${[...raw].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
1447
+ }
1448
+ async function payUptoEvm(input) {
1449
+ const { publicClient, walletClient, account, chainId, chain, accept } = input;
1450
+ let code;
1451
+ try {
1452
+ code = await publicClient.getCode({ address: account.address });
1453
+ } catch {
1454
+ code = void 0;
1455
+ }
1456
+ if (code && code !== "0x") {
1457
+ throw new UnsupportedSchemeError(
1458
+ `upto buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account. Pay via onchain-proof.`
1459
+ );
1460
+ }
1461
+ const facilitatorAddress = accept.extra?.facilitatorAddress;
1462
+ if (typeof facilitatorAddress !== "string" || facilitatorAddress.length === 0) {
1463
+ throw new UnsupportedSchemeError(
1464
+ `upto: the rail carries no extra.facilitatorAddress to bind into witness.facilitator \u2014 refusing to sign.`
1465
+ );
1466
+ }
1467
+ const token = getAddress4(accept.asset);
1468
+ const payTo = getAddress4(accept.payTo);
1469
+ const facilitator = getAddress4(facilitatorAddress);
1470
+ const value = BigInt(accept.amount);
1471
+ const approvalTx = await ensurePermit2Allowance({
1472
+ publicClient,
1473
+ walletClient,
1474
+ account,
1475
+ chain,
1476
+ token,
1477
+ amount: value
1478
+ });
1479
+ const nonce = randomPermit2Nonce2();
1480
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + accept.maxTimeoutSeconds);
1481
+ const validAfter = 0n;
1482
+ const spender = getAddress4(X402_UPTO_PERMIT2_PROXY);
1483
+ const from = account.address;
1484
+ const signature = await walletClient.signTypedData({
1485
+ account,
1486
+ domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS },
1487
+ types: PERMIT2_UPTO_WITNESS_TYPES,
1488
+ primaryType: "PermitWitnessTransferFrom",
1489
+ message: {
1490
+ permitted: { token, amount: value },
1491
+ spender,
1492
+ nonce,
1493
+ deadline,
1494
+ witness: { to: payTo, facilitator, validAfter }
1495
+ }
1496
+ });
1497
+ const permit2Authorization = {
1498
+ permitted: { token, amount: value.toString() },
1499
+ from,
1500
+ spender,
1501
+ nonce: nonce.toString(),
1502
+ deadline: deadline.toString(),
1503
+ witness: { to: payTo, facilitator, validAfter: validAfter.toString() }
1504
+ };
1505
+ return {
1506
+ payload: { signature, permit2Authorization },
1507
+ payerFrom: from,
1508
+ nonce: nonce.toString(),
1509
+ ...approvalTx ? { approvalTx } : {}
1510
+ };
1511
+ }
1512
+ function resolveUptoRailEvm(input) {
1513
+ const { asset, relayerAddress, proxySupported, domain } = input;
1514
+ if (asset === "native") return null;
1515
+ if (!proxySupported()) return null;
1516
+ const extra = { facilitatorAddress: getAddress4(relayerAddress) };
1517
+ if (domain) {
1518
+ extra.name = domain.name;
1519
+ extra.version = domain.version;
1520
+ }
1521
+ return { method: "permit2-upto", extra };
1522
+ }
1523
+ async function verifyAndSettleUptoEvm(input) {
1524
+ const { publicClient, walletClient, account, chain, payload, accept, settleAmount } = input;
1525
+ const token = getAddress4(accept.asset);
1526
+ const payTo = getAddress4(accept.payTo);
1527
+ const maxAmount = BigInt(accept.amount);
1528
+ const proxy = getAddress4(X402_UPTO_PERMIT2_PROXY);
1529
+ const relayerAddress = getAddress4(account.address);
1530
+ let from;
1531
+ let spender;
1532
+ let permittedToken;
1533
+ let witnessTo;
1534
+ let witnessFacilitator;
1535
+ let permittedAmount;
1536
+ let nonce;
1537
+ let deadline;
1538
+ let validAfter;
1539
+ const signature = payload.signature;
1540
+ try {
1541
+ const pa = payload.permit2Authorization;
1542
+ from = getAddress4(pa.from);
1543
+ spender = getAddress4(pa.spender);
1544
+ permittedToken = getAddress4(pa.permitted.token);
1545
+ witnessTo = getAddress4(pa.witness.to);
1546
+ witnessFacilitator = getAddress4(pa.witness.facilitator);
1547
+ permittedAmount = BigInt(pa.permitted.amount);
1548
+ nonce = BigInt(pa.nonce);
1549
+ deadline = BigInt(pa.deadline);
1550
+ validAfter = BigInt(pa.witness.validAfter);
1551
+ if (!/^0x[0-9a-fA-F]+$/.test(signature)) throw new Error("signature must be hex");
1552
+ } catch (err) {
1553
+ return {
1554
+ ok: false,
1555
+ error: "signature_invalid",
1556
+ detail: `Malformed upto authorization: ${err instanceof Error ? err.message : String(err)}.`
1557
+ };
1558
+ }
1559
+ if (witnessTo !== payTo) {
1560
+ return { ok: false, error: "wrong_recipient", detail: `Authorization pays witness.to ${witnessTo}, not ${payTo}.` };
1561
+ }
1562
+ if (permittedToken !== token) {
1563
+ return { ok: false, error: "signature_invalid", detail: `Authorization permits token ${permittedToken}, not the rail's ${token}.` };
1564
+ }
1565
+ if (spender !== proxy) {
1566
+ return { ok: false, error: "signature_invalid", detail: `Authorization spender ${spender} is not the x402UptoPermit2Proxy ${proxy}; it can't be settled here.` };
1567
+ }
1568
+ if (witnessFacilitator !== relayerAddress) {
1569
+ return { ok: false, error: "signature_invalid", detail: `Authorization binds facilitator ${witnessFacilitator}, not this relayer ${relayerAddress}; only the bound facilitator can settle.` };
1570
+ }
1571
+ if (permittedAmount < maxAmount) {
1572
+ return { ok: false, error: "amount_too_low", detail: `Permitted (signed MAX) ${permittedAmount} is below the rail max ${maxAmount}.` };
1573
+ }
1574
+ if (permittedAmount > maxAmount) {
1575
+ return { ok: false, error: "upto_settle_exceeds_max", detail: `Permitted (signed MAX) ${permittedAmount} exceeds the advertised rail max ${maxAmount}; sign exactly the ceiling.` };
1576
+ }
1577
+ if (settleAmount < 0n) {
1578
+ return { ok: false, error: "upto_settle_exceeds_max", detail: `Settle amount ${settleAmount} is negative.` };
1579
+ }
1580
+ if (settleAmount > permittedAmount) {
1581
+ return { ok: false, error: "upto_settle_exceeds_max", detail: `Settle amount ${settleAmount} exceeds the signed MAX ${permittedAmount}.` };
1582
+ }
1583
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1584
+ if (deadline <= now) {
1585
+ return { ok: false, error: "payment_expired", detail: `Permit2 deadline ${deadline} <= now ${now}.` };
1586
+ }
1587
+ let fromCode;
1588
+ try {
1589
+ fromCode = await publicClient.getCode({ address: from });
1590
+ } catch {
1591
+ return { ok: false, error: "tx_not_found", detail: `Could not read code at ${from} (transient RPC) \u2014 retry.` };
1592
+ }
1593
+ if (!(fromCode && fromCode !== "0x")) {
1594
+ let recovered;
1595
+ try {
1596
+ recovered = await recoverTypedDataAddress3({
1597
+ domain: { name: "Permit2", chainId: chain.id, verifyingContract: PERMIT2_ADDRESS },
1598
+ types: PERMIT2_UPTO_WITNESS_TYPES,
1599
+ primaryType: "PermitWitnessTransferFrom",
1600
+ message: {
1601
+ permitted: { token: permittedToken, amount: permittedAmount },
1602
+ spender,
1603
+ nonce,
1604
+ deadline,
1605
+ witness: { to: witnessTo, facilitator: witnessFacilitator, validAfter }
1606
+ },
1607
+ signature
1608
+ });
1609
+ } catch (err) {
1610
+ return { ok: false, error: "signature_invalid", detail: `Not a valid EIP-712 signature: ${shorten3(err instanceof Error ? err.message : String(err))}.` };
1611
+ }
1612
+ if (recovered !== from) {
1613
+ return { ok: false, error: "signature_invalid", detail: `Signature recovered to ${recovered}, not the authorizer ${from}.` };
1614
+ }
1615
+ }
1616
+ if (settleAmount === 0n) {
1617
+ return {
1618
+ ok: true,
1619
+ receipt: {
1620
+ scheme: "upto",
1621
+ success: true,
1622
+ network: accept.network,
1623
+ transaction: "",
1624
+ asset: accept.asset,
1625
+ amount: "0",
1626
+ payer: from,
1627
+ payTo: accept.payTo,
1628
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1629
+ }
1630
+ };
1631
+ }
1632
+ try {
1633
+ const word = nonce >> 8n;
1634
+ const bit = nonce & 0xffn;
1635
+ const bitmap = await publicClient.readContract({
1636
+ address: PERMIT2_ADDRESS,
1637
+ abi: permit2NonceBitmapAbi,
1638
+ functionName: "nonceBitmap",
1639
+ args: [from, word]
1640
+ });
1641
+ if ((bitmap >> bit & 1n) === 1n) {
1642
+ return { ok: false, error: "tx_already_used", detail: `Permit2 nonce ${nonce} already used or invalidated for ${from}.` };
1643
+ }
1644
+ } catch {
1645
+ return { ok: false, error: "tx_not_found", detail: "Could not read the Permit2 nonce bitmap (transient RPC) \u2014 retry." };
1646
+ }
1647
+ const settleArgs = [
1648
+ { permitted: { token: permittedToken, amount: permittedAmount }, nonce, deadline },
1649
+ settleAmount,
1650
+ from,
1651
+ { to: witnessTo, facilitator: witnessFacilitator, validAfter },
1652
+ signature
1653
+ ];
1654
+ try {
1655
+ await publicClient.simulateContract({
1656
+ account,
1657
+ address: proxy,
1658
+ abi: x402UptoProxyAbi,
1659
+ functionName: "settle",
1660
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1661
+ args: settleArgs
1662
+ });
1663
+ } catch (err) {
1664
+ const msg = err instanceof Error ? err.message : String(err);
1665
+ if (/nonce|invalidated|used/i.test(msg)) return { ok: false, error: "tx_already_used", detail: "Permit2 nonce is used or invalidated." };
1666
+ if (/expired|deadline|too early|not yet/i.test(msg)) return { ok: false, error: "payment_expired", detail: shorten3(msg) };
1667
+ if (/exceeds.*permitted|AmountExceeds/i.test(msg)) return { ok: false, error: "upto_settle_exceeds_max", detail: shorten3(msg) };
1668
+ if (/facilitator|Unauthorized/i.test(msg)) return { ok: false, error: "signature_invalid", detail: shorten3(msg) };
1669
+ if (/signature/i.test(msg)) return { ok: false, error: "signature_invalid", detail: shorten3(msg) };
1670
+ return { ok: false, error: "tx_reverted", detail: `upto settle would revert: ${shorten3(msg)}` };
1671
+ }
1672
+ let txHash;
1673
+ try {
1674
+ txHash = await walletClient.writeContract({
1675
+ account,
1676
+ chain,
1677
+ address: proxy,
1678
+ abi: x402UptoProxyAbi,
1679
+ functionName: "settle",
1680
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1681
+ args: settleArgs
1682
+ });
1683
+ } catch (err) {
1684
+ throw new SettlementError(
1685
+ `upto settle: the merchant relayer failed to broadcast the proxy settle (${shorten3(err instanceof Error ? err.message : String(err))}). The payer's signature is still valid and its nonce unused \u2014 fund/fix the relayer and the payer can retry.`,
1686
+ { cause: err }
1687
+ );
1688
+ }
1689
+ try {
1690
+ const confirmations = accept.extra.minConfirmations ?? 1;
1691
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations });
1692
+ if (receipt.status !== "success") {
1693
+ return { ok: false, error: "tx_reverted", detail: `Settlement tx ${txHash} reverted on-chain.` };
1694
+ }
1695
+ } catch (err) {
1696
+ throw new SettlementError(
1697
+ `upto settle: broadcast ${txHash} but couldn't confirm it (${shorten3(err instanceof Error ? err.message : String(err))}).`,
1698
+ { cause: err }
1699
+ );
1700
+ }
1701
+ return {
1702
+ ok: true,
1703
+ receipt: {
1704
+ scheme: "upto",
1705
+ success: true,
1706
+ network: accept.network,
1707
+ transaction: txHash,
1708
+ asset: accept.asset,
1709
+ // The ACTUAL settled amount (≤ max) — what the buyer reads back to record metered spend.
1710
+ amount: settleAmount.toString(),
1711
+ payer: from,
1712
+ payTo: accept.payTo,
1713
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1714
+ }
1715
+ };
1716
+ }
1717
+
1356
1718
  // src/x402.ts
1357
1719
  var HEADER_REQUIRED = "payment-required";
1358
1720
  var HEADER_SIGNATURE = "payment-signature";
@@ -1386,6 +1748,9 @@ function fromBase64Json(b64) {
1386
1748
  return null;
1387
1749
  }
1388
1750
  }
1751
+ function decodeBase64Json(value) {
1752
+ return fromBase64Json(value);
1753
+ }
1389
1754
  function toBase64Json(value) {
1390
1755
  return encodeBase64(JSON.stringify(value));
1391
1756
  }
@@ -1401,8 +1766,15 @@ function chainIdFromNetwork(network) {
1401
1766
  function buildChallengeHeader(challenge) {
1402
1767
  return toBase64Json(challenge);
1403
1768
  }
1404
- function buildReceiptHeader(receipt) {
1405
- return toBase64Json(receipt);
1769
+ function buildReceiptHeader(receipt, extensions) {
1770
+ return toBase64Json(extensions ? { ...receipt, extensions } : receipt);
1771
+ }
1772
+ var EXT_OFFER_RECEIPT = "offer-receipt";
1773
+ function buildReceiptExtension(bundle) {
1774
+ const info = { settlement: bundle.receipt, resource: bundle.resource };
1775
+ if (typeof bundle.decimals === "number") info.decimals = bundle.decimals;
1776
+ if (bundle.attestation) info.receipt = bundle.attestation;
1777
+ return { [EXT_OFFER_RECEIPT]: { info } };
1406
1778
  }
1407
1779
  function buildSignatureHeader(signature) {
1408
1780
  return toBase64Json(signature);
@@ -1410,6 +1782,9 @@ function buildSignatureHeader(signature) {
1410
1782
  function buildExactSignatureHeader(input) {
1411
1783
  return toBase64Json({ x402Version: 2, accepted: input.accepted, payload: input.payload });
1412
1784
  }
1785
+ function buildUptoSignatureHeader(input) {
1786
+ return toBase64Json({ x402Version: 2, accepted: input.accepted, payload: input.payload });
1787
+ }
1413
1788
  async function parseChallenge(response) {
1414
1789
  const headerValue = response.headers.get(HEADER_REQUIRED);
1415
1790
  if (headerValue) {
@@ -1429,6 +1804,27 @@ function parseReceipt(response) {
1429
1804
  const parsed = fromBase64Json(headerValue);
1430
1805
  return isValidReceipt(parsed) ? parsed : null;
1431
1806
  }
1807
+ function parseReceiptExtension(response) {
1808
+ const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
1809
+ if (!headerValue) return null;
1810
+ const parsed = fromBase64Json(headerValue);
1811
+ if (!parsed || typeof parsed !== "object") return null;
1812
+ const ext = parsed.extensions;
1813
+ const block = ext?.[EXT_OFFER_RECEIPT];
1814
+ const info = block?.info;
1815
+ if (!info) return null;
1816
+ const settlement = isValidReceipt(info.settlement) ? info.settlement : isValidReceipt(info.receipt) ? info.receipt : void 0;
1817
+ if (!settlement) return null;
1818
+ const attestation = info.receipt && info.receipt !== settlement ? info.receipt : void 0;
1819
+ const res = info.resource;
1820
+ return {
1821
+ piprail: "1",
1822
+ receipt: settlement,
1823
+ resource: { url: res && typeof res.url === "string" ? res.url : "" },
1824
+ ...typeof info.decimals === "number" ? { decimals: info.decimals } : {},
1825
+ ...attestation ? { attestation } : {}
1826
+ };
1827
+ }
1432
1828
  function parseSettleResponse(response) {
1433
1829
  const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
1434
1830
  if (!headerValue) return null;
@@ -1439,11 +1835,13 @@ function parseSettleResponse(response) {
1439
1835
  ...typeof parsed.transaction === "string" ? { transaction: parsed.transaction } : {},
1440
1836
  ...typeof parsed.network === "string" ? { network: parsed.network } : {},
1441
1837
  ...typeof parsed.payer === "string" ? { payer: parsed.payer } : {},
1442
- ...typeof parsed.errorReason === "string" ? { errorReason: parsed.errorReason } : {}
1838
+ ...typeof parsed.errorReason === "string" ? { errorReason: parsed.errorReason } : {},
1839
+ // The upto SettleResponse's REQUIRED `amount` (actual settled atomic units) — string
1840
+ // passthrough so the upto buyer records ACTUAL, not MAX. Absent on onchain-proof/exact.
1841
+ ...typeof parsed.amount === "string" ? { amount: parsed.amount } : {}
1443
1842
  };
1444
1843
  }
1445
- function parseSignatureHeader(value) {
1446
- const parsed = fromBase64Json(value);
1844
+ function parseSignatureObject(parsed) {
1447
1845
  if (!parsed || typeof parsed !== "object") return null;
1448
1846
  const v = parsed;
1449
1847
  const accepted = v.accepted;
@@ -1455,8 +1853,13 @@ function parseSignatureHeader(value) {
1455
1853
  }
1456
1854
  return parsed;
1457
1855
  }
1856
+ function parseSignatureHeader(value) {
1857
+ return parseSignatureObject(fromBase64Json(value));
1858
+ }
1458
1859
  function parseExactPaymentHeader(value) {
1459
- const parsed = fromBase64Json(value);
1860
+ return parseExactObject(fromBase64Json(value));
1861
+ }
1862
+ function parseExactObject(parsed) {
1460
1863
  if (!parsed || typeof parsed !== "object") return null;
1461
1864
  const v = parsed;
1462
1865
  const accepted = v.accepted ?? null;
@@ -1521,6 +1924,42 @@ function parseExactPaymentHeader(value) {
1521
1924
  }
1522
1925
  return null;
1523
1926
  }
1927
+ function parseUptoPaymentHeader(value) {
1928
+ return parseUptoObject(fromBase64Json(value));
1929
+ }
1930
+ function parseUptoObject(parsed) {
1931
+ if (!parsed || typeof parsed !== "object") return null;
1932
+ const v = parsed;
1933
+ const accepted = v.accepted ?? null;
1934
+ const scheme = accepted?.scheme ?? v.scheme;
1935
+ if (scheme !== "upto") return null;
1936
+ const network = accepted?.network ?? v.network;
1937
+ if (typeof network !== "string") return null;
1938
+ const payload = v.payload;
1939
+ if (!payload || typeof payload !== "object") return null;
1940
+ const signature = payload.signature;
1941
+ if (typeof signature !== "string") return null;
1942
+ const p2 = payload.permit2Authorization;
1943
+ if (!p2 || typeof p2 !== "object") return null;
1944
+ const permitted = p2.permitted;
1945
+ const witness = p2.witness;
1946
+ if (!permitted || typeof permitted !== "object" || !witness || typeof witness !== "object") return null;
1947
+ if (typeof permitted.token !== "string" || typeof permitted.amount !== "string") return null;
1948
+ if (typeof witness.to !== "string" || typeof witness.facilitator !== "string" || typeof witness.validAfter !== "string") {
1949
+ return null;
1950
+ }
1951
+ for (const k of ["from", "spender", "nonce", "deadline"]) {
1952
+ if (typeof p2[k] !== "string") return null;
1953
+ }
1954
+ const x402Version = typeof v.x402Version === "number" ? v.x402Version : 2;
1955
+ const asset = accepted && typeof accepted.asset === "string" ? accepted.asset : void 0;
1956
+ const base2 = { x402Version, network, ...asset ? { asset } : {}, raw: v };
1957
+ return {
1958
+ ...base2,
1959
+ method: "permit2-upto",
1960
+ payload: { signature, permit2Authorization: p2 }
1961
+ };
1962
+ }
1524
1963
  function isValidChallenge(value) {
1525
1964
  if (!value || typeof value !== "object") return false;
1526
1965
  const v = value;
@@ -1533,7 +1972,7 @@ function isValidChallenge(value) {
1533
1972
  function isValidReceipt(value) {
1534
1973
  if (!value || typeof value !== "object") return false;
1535
1974
  const v = value;
1536
- if (v.scheme !== "onchain-proof" && v.scheme !== "exact") return false;
1975
+ if (v.scheme !== "onchain-proof" && v.scheme !== "exact" && v.scheme !== "upto") return false;
1537
1976
  if (typeof v.transaction !== "string" && typeof v.txHash !== "string") return false;
1538
1977
  if (typeof v.payer !== "string") return false;
1539
1978
  return true;
@@ -1611,12 +2050,12 @@ function makeEvmNetwork(resolved) {
1611
2050
  }
1612
2051
  let normalized;
1613
2052
  try {
1614
- normalized = getAddress4(asset);
2053
+ normalized = getAddress5(asset);
1615
2054
  } catch {
1616
2055
  return null;
1617
2056
  }
1618
2057
  for (const info of Object.values(resolved.tokens)) {
1619
- if (getAddress4(info.address) === normalized) {
2058
+ if (getAddress5(info.address) === normalized) {
1620
2059
  return { symbol: info.symbol, decimals: info.decimals };
1621
2060
  }
1622
2061
  }
@@ -1716,7 +2155,7 @@ function makeEvmNetwork(resolved) {
1716
2155
  let token = null;
1717
2156
  try {
1718
2157
  token = await publicClient.readContract({
1719
- address: getAddress4(asset),
2158
+ address: getAddress5(asset),
1720
2159
  abi: erc20Abi4,
1721
2160
  functionName: "balanceOf",
1722
2161
  args: [owner]
@@ -1741,6 +2180,12 @@ function makeEvmNetwork(resolved) {
1741
2180
  signMessage: (message) => a.walletClient.signMessage({ account: a.account, message })
1742
2181
  };
1743
2182
  },
2183
+ // Tier-2 service-delivery attestation (EVM-only) — sign the official offer-receipt
2184
+ // EIP-712 RECEIPT_TYPES with the bound (payTo) wallet. Chain-independent (domain
2185
+ // chainId is hardcoded 1); viem lives in ./receipt.ts (a lazy chunk).
2186
+ signReceipt(wallet, input) {
2187
+ return signReceiptEvm(wallet, input);
2188
+ },
1744
2189
  async verify(ref, accept) {
1745
2190
  return verifyEvm({
1746
2191
  publicClient,
@@ -1824,6 +2269,43 @@ function makeEvmNetwork(resolved) {
1824
2269
  payload,
1825
2270
  accept
1826
2271
  });
2272
+ },
2273
+ // Standard x402 `upto` (metered) rail — EVM-Permit2 ONLY. The metered sibling of the
2274
+ // exact trio. resolveUptoRail advertises (Permit2-only, proxy-gated, relayer = facilitator);
2275
+ // payUpto signs the MAX with witness.facilitator bound; settleUptoSelf self-settles the actual.
2276
+ async resolveUptoRail({ asset, relayer }) {
2277
+ const relayerAddress = relayer._native.account.address;
2278
+ const domain = asset === "native" ? null : await readExactDomain(publicClient, asset).catch(() => null);
2279
+ return resolveUptoRailEvm({
2280
+ asset,
2281
+ relayerAddress,
2282
+ proxySupported: () => isUptoProxyChain(resolved.chainId),
2283
+ domain
2284
+ });
2285
+ },
2286
+ async payUpto(wallet, accept) {
2287
+ const a = wallet._native;
2288
+ const { payload, payerFrom, nonce } = await payUptoEvm({
2289
+ publicClient,
2290
+ walletClient: a.walletClient,
2291
+ account: a.account,
2292
+ chainId: resolved.chainId,
2293
+ chain: resolved.chain,
2294
+ accept
2295
+ });
2296
+ return { payload, accepted: accept, payerFrom, nonce };
2297
+ },
2298
+ async settleUptoSelf({ relayer, payload, accept, settleAmount }) {
2299
+ const a = relayer._native;
2300
+ return verifyAndSettleUptoEvm({
2301
+ publicClient,
2302
+ walletClient: a.walletClient,
2303
+ account: a.account,
2304
+ chain: resolved.chain,
2305
+ payload,
2306
+ accept,
2307
+ settleAmount
2308
+ });
1827
2309
  }
1828
2310
  };
1829
2311
  }
@@ -1840,7 +2322,7 @@ var loaders = {
1840
2322
  solana: async () => {
1841
2323
  let mod;
1842
2324
  try {
1843
- mod = await import("./solana-Z34OWXMO.js");
2325
+ mod = await import("./solana-ELUWO6N5.js");
1844
2326
  } catch (cause) {
1845
2327
  throw new MissingDriverError(
1846
2328
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -1852,7 +2334,7 @@ var loaders = {
1852
2334
  ton: async () => {
1853
2335
  let mod;
1854
2336
  try {
1855
- mod = await import("./ton-N7KSPSAR.js");
2337
+ mod = await import("./ton-7GKCTC5H.js");
1856
2338
  } catch (cause) {
1857
2339
  throw new MissingDriverError(
1858
2340
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -1864,7 +2346,7 @@ var loaders = {
1864
2346
  stellar: async () => {
1865
2347
  let mod;
1866
2348
  try {
1867
- mod = await import("./stellar-PWC4LETJ.js");
2349
+ mod = await import("./stellar-ASP2THL2.js");
1868
2350
  } catch (cause) {
1869
2351
  throw new MissingDriverError(
1870
2352
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -1876,7 +2358,7 @@ var loaders = {
1876
2358
  xrpl: async () => {
1877
2359
  let mod;
1878
2360
  try {
1879
- mod = await import("./xrpl-6B4IYRWI.js");
2361
+ mod = await import("./xrpl-DD7TJL5L.js");
1880
2362
  } catch (cause) {
1881
2363
  throw new MissingDriverError(
1882
2364
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -1888,7 +2370,7 @@ var loaders = {
1888
2370
  tron: async () => {
1889
2371
  let mod;
1890
2372
  try {
1891
- mod = await import("./tron-WKDUQVZR.js");
2373
+ mod = await import("./tron-BMCWN5SS.js");
1892
2374
  } catch (cause) {
1893
2375
  throw new MissingDriverError(
1894
2376
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -1900,7 +2382,7 @@ var loaders = {
1900
2382
  sui: async () => {
1901
2383
  let mod;
1902
2384
  try {
1903
- mod = await import("./sui-VEOGHBYE.js");
2385
+ mod = await import("./sui-FZIKZNVI.js");
1904
2386
  } catch (cause) {
1905
2387
  throw new MissingDriverError(
1906
2388
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -1912,7 +2394,7 @@ var loaders = {
1912
2394
  near: async () => {
1913
2395
  let mod;
1914
2396
  try {
1915
- mod = await import("./near-QV4IOZNX.js");
2397
+ mod = await import("./near-FZBUICCS.js");
1916
2398
  } catch (cause) {
1917
2399
  throw new MissingDriverError(
1918
2400
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -1924,7 +2406,7 @@ var loaders = {
1924
2406
  aptos: async () => {
1925
2407
  let mod;
1926
2408
  try {
1927
- mod = await import("./aptos-WMPFEASY.js");
2409
+ mod = await import("./aptos-LY67Q6QF.js");
1928
2410
  } catch (cause) {
1929
2411
  throw new MissingDriverError(
1930
2412
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -1936,7 +2418,7 @@ var loaders = {
1936
2418
  algorand: async () => {
1937
2419
  let mod;
1938
2420
  try {
1939
- mod = await import("./algorand-WJL6VRXW.js");
2421
+ mod = await import("./algorand-AA3WXKW4.js");
1940
2422
  } catch (cause) {
1941
2423
  throw new MissingDriverError(
1942
2424
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -2582,6 +3064,53 @@ function encodeBase642(str) {
2582
3064
  }
2583
3065
 
2584
3066
  // src/policy.ts
3067
+ var DENOM_PRECISION = MAX_DECIMALS;
3068
+ var BUILTIN_DENOMS = {
3069
+ USDC: "USD",
3070
+ USDT: "USD",
3071
+ USD1: "USD",
3072
+ FDUSD: "USD",
3073
+ RLUSD: "USD",
3074
+ EURC: "EUR"
3075
+ };
3076
+ function denomOf(symbol, asset, policy) {
3077
+ if (asset === "native") return void 0;
3078
+ const norm = (v) => typeof v === "string" && v.trim() !== "" ? v.trim().toUpperCase() : void 0;
3079
+ const override = policy?.denomFor;
3080
+ if (override && typeof override === "object") {
3081
+ if (asset && asset in override) {
3082
+ const d = norm(override[asset]);
3083
+ if (d) return d;
3084
+ }
3085
+ if (symbol) {
3086
+ const hit = Object.entries(override).find(([k]) => k.trim().toUpperCase() === symbol.trim().toUpperCase());
3087
+ if (hit) {
3088
+ const d = norm(hit[1]);
3089
+ if (d) return d;
3090
+ }
3091
+ }
3092
+ }
3093
+ if (symbol) {
3094
+ const builtin = BUILTIN_DENOMS[symbol.trim().toUpperCase()];
3095
+ if (builtin) return builtin;
3096
+ }
3097
+ return void 0;
3098
+ }
3099
+ function scaleToDenom(amountBase, decimals) {
3100
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > DENOM_PRECISION) return null;
3101
+ return amountBase * 10n ** BigInt(DENOM_PRECISION - decimals);
3102
+ }
3103
+ function capForDenom(caps, denom) {
3104
+ if (!caps || typeof caps !== "object") return void 0;
3105
+ const target = denom.trim().toUpperCase();
3106
+ let best;
3107
+ for (const [k, v] of Object.entries(caps)) {
3108
+ if (typeof k !== "string" || typeof v !== "string") continue;
3109
+ if (k.trim().toUpperCase() !== target) continue;
3110
+ if (best === void 0 || Number(v) < Number(best)) best = v;
3111
+ }
3112
+ return best;
3113
+ }
2585
3114
  var ALLOW = { allowed: true };
2586
3115
  var deny = (code, reason) => ({
2587
3116
  allowed: false,
@@ -2665,6 +3194,34 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
2665
3194
  );
2666
3195
  }
2667
3196
  }
3197
+ if (ctx && policy.maxTotalPerDenom) {
3198
+ const denom = denomOf(intent.symbol, intent.asset, policy);
3199
+ const capStr = denom ? capForDenom(policy.maxTotalPerDenom, denom) : void 0;
3200
+ if (denom && capStr !== void 0) {
3201
+ const thisScaled = scaleToDenom(intent.amountBase, intent.decimals);
3202
+ if (thisScaled === null) {
3203
+ return deny(
3204
+ "MAX_TOTAL_DENOM",
3205
+ `token for ${denom} has more than ${DENOM_PRECISION} decimals \u2014 refusing to count it toward policy.maxTotalPerDenom.${denom} on trust (it can't be summed safely).`
3206
+ );
3207
+ }
3208
+ const capScaled = floorUnits(capStr, DENOM_PRECISION);
3209
+ if ((ctx.spentInDenomScaled ?? 0n) + thisScaled > capScaled) {
3210
+ return deny(
3211
+ "MAX_TOTAL_DENOM",
3212
+ `this payment would push total ${denom} spend past policy.maxTotalPerDenom.${denom} (${capStr}) \u2014 summed across every ${denom} token and chain.`
3213
+ );
3214
+ }
3215
+ }
3216
+ }
3217
+ if (ctx && policy.maxPayments !== void 0) {
3218
+ if ((ctx.paymentCount ?? 0) + 1 > policy.maxPayments) {
3219
+ return deny(
3220
+ "MAX_PAYMENTS",
3221
+ `this payment would exceed policy.maxPayments (${policy.maxPayments}); already settled ${ctx.paymentCount ?? 0}.`
3222
+ );
3223
+ }
3224
+ }
2668
3225
  if (ctx && policy.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
2669
3226
  const cap = floorUnits(policy.windowTotal, intent.decimals);
2670
3227
  if (ctx.spentInWindowBase + intent.amountBase > cap) {
@@ -2674,6 +3231,14 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
2674
3231
  );
2675
3232
  }
2676
3233
  }
3234
+ if (ctx && policy.maxPaymentsPerWindow !== void 0 && policy.windowSeconds !== void 0) {
3235
+ if ((ctx.paymentCountInWindow ?? 0) + 1 > policy.maxPaymentsPerWindow) {
3236
+ return deny(
3237
+ "WINDOW_COUNT",
3238
+ `this payment would exceed policy.maxPaymentsPerWindow (${policy.maxPaymentsPerWindow}) within the last ${policy.windowSeconds}s.`
3239
+ );
3240
+ }
3241
+ }
2677
3242
  return ALLOW;
2678
3243
  }
2679
3244
 
@@ -2682,37 +3247,135 @@ var keyFor = (network, asset) => `${network}|${asset}`;
2682
3247
  var SpendLedger = class {
2683
3248
  records = [];
2684
3249
  buckets = /* @__PURE__ */ new Map();
3250
+ /** Per-denomination running total, scaled to {@link DENOM_PRECISION}. Keyed by the
3251
+ * UPPERCASE denomination so lookups are case-insensitive. */
3252
+ denomTotals = /* @__PURE__ */ new Map();
3253
+ /** Threshold keys already warned (`warnAtFraction` fires once per crossing per cap).
3254
+ * Lives on the LEDGER — not the client — so clients SHARING one (a cross-chain
3255
+ * MultiChainPayer) dedupe together: a denomination/count threshold fires once across the
3256
+ * whole shared budget, not once per chain. */
3257
+ warned = /* @__PURE__ */ new Set();
3258
+ store;
2685
3259
  /**
2686
3260
  * Session clock origin (epoch-ms) — process/session start = ledger
2687
3261
  * construction. In-memory; a new process is a new session. The client reads it
2688
3262
  * to compute the `ttlSeconds` deadline and the rolling-window slice.
2689
3263
  */
2690
3264
  sessionStart = Date.now();
2691
- /** Record a settled payment. `decimals` is the TRUE token decimals (for the
2692
- * per-asset running total used by maxTotal + the formatted summary). */
2693
- record(r, decimals) {
2694
- this.records.push(r);
2695
- const key = keyFor(r.network, r.asset);
3265
+ /**
3266
+ * @param store Optional durable {@link SpendStore}. When supplied, the ledger
3267
+ * HYDRATES from `store.load()` here (so prior spend resumes after a restart) and
3268
+ * `append()`s every settled payment. A throwing/absent store fails SAFE to an
3269
+ * empty in-memory ledger — it never blocks construction (ERRORS.md: never throw).
3270
+ */
3271
+ constructor(store) {
3272
+ this.store = store;
3273
+ if (store) {
3274
+ let seed = [];
3275
+ try {
3276
+ seed = store.load() ?? [];
3277
+ } catch {
3278
+ seed = [];
3279
+ }
3280
+ for (const r of seed) this.ingest(r, r?.decimals ?? 0, r?.denom);
3281
+ }
3282
+ }
3283
+ /**
3284
+ * A record is safe to tally iff its `amountBase` is a non-negative integer STRING and its
3285
+ * `decimals` is an integer in `[0, MAX_DECIMALS]`. The live `record()` path always passes
3286
+ * (the client validated the quote), but a hydrated record comes from an UNTRUSTED store
3287
+ * (a tampered/corrupt/future-version JSONL line), so we gate it here — a bad record is
3288
+ * dropped rather than allowed to throw `BigInt(...)`/`formatUnits(...)` later and brick a
3289
+ * read or the constructor.
3290
+ */
3291
+ isTallyable(r, decimals) {
3292
+ return !!r && typeof r.amountBase === "string" && /^\d+$/.test(r.amountBase) && Number.isInteger(decimals) && decimals >= 0 && decimals <= MAX_DECIMALS;
3293
+ }
3294
+ /** Apply a record to the in-memory tallies (records + per-asset bucket + denom total).
3295
+ * Shared by {@link record} and constructor hydration; does NOT persist. Returns the
3296
+ * stored record, or `null` when the record is corrupt and was skipped. */
3297
+ ingest(r, decimals, denom) {
3298
+ if (!this.isTallyable(r, decimals)) return null;
3299
+ const denomStr = typeof denom === "string" && denom.trim() !== "" ? denom : void 0;
3300
+ const rec = { ...r, decimals, ...denomStr ? { denom: denomStr } : {} };
3301
+ this.records.push(rec);
3302
+ const key = keyFor(rec.network, rec.asset);
2696
3303
  const bucket = this.buckets.get(key);
2697
3304
  if (bucket) {
2698
- bucket.total += BigInt(r.amountBase);
3305
+ bucket.total += BigInt(rec.amountBase);
2699
3306
  bucket.count += 1;
2700
- if (!bucket.symbol && r.symbol) bucket.symbol = r.symbol;
3307
+ if (!bucket.symbol && rec.symbol) bucket.symbol = rec.symbol;
2701
3308
  } else {
2702
3309
  this.buckets.set(key, {
2703
- network: r.network,
2704
- asset: r.asset,
2705
- symbol: r.symbol,
3310
+ network: rec.network,
3311
+ asset: rec.asset,
3312
+ symbol: rec.symbol,
2706
3313
  decimals,
2707
- total: BigInt(r.amountBase),
3314
+ total: BigInt(rec.amountBase),
2708
3315
  count: 1
2709
3316
  });
2710
3317
  }
3318
+ if (denomStr) {
3319
+ const scaled = scaleToDenom(BigInt(rec.amountBase), decimals);
3320
+ if (scaled !== null) {
3321
+ const k = denomStr.toUpperCase();
3322
+ this.denomTotals.set(k, (this.denomTotals.get(k) ?? 0n) + scaled);
3323
+ }
3324
+ }
3325
+ return rec;
3326
+ }
3327
+ /** Record a settled payment. `decimals` is the TRUE token decimals (for the
3328
+ * per-asset running total + the formatted summary). `denom` is the unit-of-account
3329
+ * the payment counts toward in the grand total (or omit for none). Persists to the
3330
+ * {@link SpendStore} when one is configured (a failed append never throws). */
3331
+ record(r, decimals, denom) {
3332
+ const rec = this.ingest(r, decimals, denom);
3333
+ if (rec && this.store) {
3334
+ try {
3335
+ this.store.append(rec);
3336
+ } catch {
3337
+ }
3338
+ }
2711
3339
  }
2712
3340
  /** Running total (base units) already spent on this (network, asset). */
2713
3341
  totalFor(network, asset) {
2714
3342
  return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
2715
3343
  }
3344
+ /**
3345
+ * Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
3346
+ * tokens of different decimals add up exactly). Summed across every token of that
3347
+ * denomination and every chain this ledger has seen. Powers `maxTotalPerDenom`.
3348
+ * `0n` for a denomination never spent on. Case-insensitive.
3349
+ */
3350
+ totalForDenom(denom) {
3351
+ return this.denomTotals.get(denom.toUpperCase()) ?? 0n;
3352
+ }
3353
+ /** Total number of settled payments (across every chain + token). Powers `maxPayments`. */
3354
+ count() {
3355
+ return this.records.length;
3356
+ }
3357
+ /** Mark a `warnAtFraction` threshold key as fired; returns `true` the FIRST time (so the
3358
+ * caller emits the `budget-threshold` event once) and `false` thereafter. Shared across
3359
+ * every client on this ledger, so a cross-chain threshold fires once for the whole budget. */
3360
+ markWarned(key) {
3361
+ if (this.warned.has(key)) return false;
3362
+ this.warned.add(key);
3363
+ return true;
3364
+ }
3365
+ /**
3366
+ * Number of settled payments whose `at` (ISO) is at or after `sinceMs` (epoch-ms),
3367
+ * across every chain + token. Backs the rolling payment-count cap
3368
+ * (`maxPaymentsPerWindow`, `sinceMs = now - windowSeconds*1000`). Linear scan —
3369
+ * negligible at agent-session cardinality and only when a window count cap is set.
3370
+ */
3371
+ countSince(sinceMs) {
3372
+ let n = 0;
3373
+ for (const r of this.records) {
3374
+ const t = Date.parse(r.at);
3375
+ if (Number.isNaN(t) || t >= sinceMs) n += 1;
3376
+ }
3377
+ return n;
3378
+ }
2716
3379
  /**
2717
3380
  * Sum of base-unit amounts for (network, asset) whose record `at` (ISO
2718
3381
  * timestamp) is at or after `sinceMs` (epoch-ms). Backs the rolling window
@@ -2723,9 +3386,9 @@ var SpendLedger = class {
2723
3386
  totalSince(network, asset, sinceMs) {
2724
3387
  let sum = 0n;
2725
3388
  for (const r of this.records) {
2726
- if (r.network === network && r.asset === asset && Date.parse(r.at) >= sinceMs) {
2727
- sum += BigInt(r.amountBase);
2728
- }
3389
+ if (r.network !== network || r.asset !== asset) continue;
3390
+ const t = Date.parse(r.at);
3391
+ if (Number.isNaN(t) || t >= sinceMs) sum += BigInt(r.amountBase);
2729
3392
  }
2730
3393
  return sum;
2731
3394
  }
@@ -2745,6 +3408,14 @@ var SpendLedger = class {
2745
3408
  totalBase: b.total
2746
3409
  }));
2747
3410
  }
3411
+ /**
3412
+ * The per-denomination grand totals, as read-only tuples — `denom` and the running
3413
+ * `totalScaled` (at {@link DENOM_PRECISION}). Lets the client compose the grand-total
3414
+ * budget view; the cap math lives in the client. A never-spent denomination is absent.
3415
+ */
3416
+ denomBuckets() {
3417
+ return [...this.denomTotals.entries()].map(([denom, totalScaled]) => ({ denom, totalScaled }));
3418
+ }
2748
3419
  /** An immutable snapshot of all spend so far. */
2749
3420
  summary() {
2750
3421
  return {
@@ -2758,6 +3429,12 @@ var SpendLedger = class {
2758
3429
  totalFormatted: formatUnits(b.total, b.decimals),
2759
3430
  count: b.count
2760
3431
  })),
3432
+ byDenom: [...this.denomTotals.entries()].map(([denom, scaled]) => ({
3433
+ denom,
3434
+ totalScaled: scaled.toString(),
3435
+ totalFormatted: formatUnits(scaled, DENOM_PRECISION),
3436
+ count: this.records.filter((r) => r.denom?.toUpperCase() === denom).length
3437
+ })),
2761
3438
  records: [...this.records]
2762
3439
  };
2763
3440
  }
@@ -2771,24 +3448,41 @@ var RECIPIENT_FIX = {
2771
3448
  NOT_OPTED_IN: "the recipient must opt into this asset once (a 0-amount self-transfer)",
2772
3449
  INACTIVE: "the recipient account doesn't exist yet \u2014 fund it with the chain's base reserve to activate it"
2773
3450
  };
3451
+ var RECEIPT_VERIFY_WINDOW_SECONDS = 100 * 365 * 24 * 60 * 60;
2774
3452
  var PipRailClient = class {
2775
3453
  opts;
2776
3454
  maxRetries;
2777
3455
  retryTimeoutMs;
2778
3456
  onEvent;
2779
- // Per-asset tally of everything this client has paid (powers spent() and the
2780
- // policy's maxTotal cap).
2781
- ledger = new SpendLedger();
3457
+ // Per-asset (+ per-denomination + count) tally of everything this client has paid
3458
+ // powers spent()/budget() and the maxTotal/maxTotalPerDenom/maxPayments caps. Either
3459
+ // its own (optionally store-backed) ledger, or a SHARED one injected so the grand
3460
+ // total + count caps span several chains (MultiChainPayer.fromWallets).
3461
+ ledger;
3462
+ // `warnAtFraction` threshold dedup lives on the LEDGER (`ledger.markWarned`), so clients
3463
+ // sharing one ledger (a cross-chain MultiChainPayer) fire each threshold once for the whole
3464
+ // shared budget — not once per chain.
2782
3465
  // Resolved lazily on first request — this is what lets Solana (and future
2783
3466
  // families) auto-mount with no setup call.
2784
3467
  bound;
3468
+ // The verifiable receipt from the most recent settled fetch (null if the server emitted
3469
+ // none). Captured pure (no chain read) and surfaced via lastReceipt(); the resource URL is
3470
+ // stamped from the URL this client actually fetched (authoritative over the gate's default).
3471
+ lastReceiptValue = null;
2785
3472
  constructor(opts) {
2786
3473
  this.opts = opts;
2787
3474
  this.maxRetries = Math.max(1, opts.maxPaymentRetries ?? 3);
2788
3475
  this.retryTimeoutMs = opts.retryTimeoutMs ?? 3e4;
2789
3476
  this.onEvent = opts.onEvent ?? (() => void 0);
3477
+ if (opts.ledger && opts.spendStore) {
3478
+ throw new TypeError(
3479
+ "Pass either `ledger` (a shared SpendLedger) or `spendStore`, not both \u2014 a shared ledger already owns its store."
3480
+ );
3481
+ }
3482
+ this.ledger = opts.ledger ?? new SpendLedger(opts.spendStore);
2790
3483
  this.assertPolicyAmountCaps(opts.policy);
2791
3484
  this.assertPolicyTimeOptions(opts.policy);
3485
+ this.assertPolicySpendControls(opts.policy);
2792
3486
  }
2793
3487
  /**
2794
3488
  * Fail LOUDLY at construction on a malformed amount cap — a security boundary
@@ -2809,6 +3503,65 @@ var PipRailClient = class {
2809
3503
  );
2810
3504
  }
2811
3505
  }
3506
+ if (policy.maxTotalPerDenom !== void 0) {
3507
+ const m = policy.maxTotalPerDenom;
3508
+ const proto = m && typeof m === "object" ? Object.getPrototypeOf(m) : false;
3509
+ if (!m || typeof m !== "object" || Array.isArray(m) || proto !== Object.prototype && proto !== null) {
3510
+ throw new TypeError(
3511
+ `policy.maxTotalPerDenom must be a plain { DENOM: amount } object (e.g. { USD: '20.00' }); got ${JSON.stringify(policy.maxTotalPerDenom)}.`
3512
+ );
3513
+ }
3514
+ for (const [denom, v] of Object.entries(policy.maxTotalPerDenom)) {
3515
+ if (denom.trim() === "") {
3516
+ throw new TypeError("policy.maxTotalPerDenom has a blank denomination key.");
3517
+ }
3518
+ if (typeof v !== "string" || !/^\d+(\.\d+)?$/.test(v)) {
3519
+ throw new TypeError(
3520
+ `policy.maxTotalPerDenom.${denom} must be a non-negative decimal string (e.g. '20.00'); got ${JSON.stringify(v)}.`
3521
+ );
3522
+ }
3523
+ }
3524
+ }
3525
+ if (policy.denomFor !== void 0) {
3526
+ const m = policy.denomFor;
3527
+ if (!m || typeof m !== "object" || Array.isArray(m)) {
3528
+ throw new TypeError(`policy.denomFor must be a { token: DENOM } object; got ${JSON.stringify(m)}.`);
3529
+ }
3530
+ for (const [k, v] of Object.entries(policy.denomFor)) {
3531
+ if (typeof v !== "string" || v.trim() === "") {
3532
+ throw new TypeError(`policy.denomFor.${k} must be a non-empty denomination string; got ${JSON.stringify(v)}.`);
3533
+ }
3534
+ }
3535
+ }
3536
+ }
3537
+ /**
3538
+ * Fail LOUDLY at construction on a misconfigured count / threshold control (a
3539
+ * programmer error → `TypeError`): payment counts must be positive safe integers,
3540
+ * `maxPaymentsPerWindow` needs `windowSeconds` (it shares the window), and
3541
+ * `warnAtFraction` must be in (0, 1].
3542
+ */
3543
+ assertPolicySpendControls(policy) {
3544
+ if (!policy) return;
3545
+ for (const field of ["maxPayments", "maxPaymentsPerWindow"]) {
3546
+ const v = policy[field];
3547
+ if (v === void 0) continue;
3548
+ if (!Number.isSafeInteger(v) || v <= 0) {
3549
+ throw new TypeError(`policy.${field} must be a positive integer; got ${JSON.stringify(v)}.`);
3550
+ }
3551
+ }
3552
+ if (policy.maxPaymentsPerWindow !== void 0 && policy.windowSeconds === void 0) {
3553
+ throw new TypeError(
3554
+ "policy.maxPaymentsPerWindow needs policy.windowSeconds (the rolling window it counts within) \u2014 set both, or neither."
3555
+ );
3556
+ }
3557
+ if (policy.warnAtFraction !== void 0) {
3558
+ const f = policy.warnAtFraction;
3559
+ if (typeof f !== "number" || !(f > 0 && f <= 1)) {
3560
+ throw new TypeError(
3561
+ `policy.warnAtFraction must be a number in (0, 1] (e.g. 0.8); got ${JSON.stringify(f)}.`
3562
+ );
3563
+ }
3564
+ }
2812
3565
  }
2813
3566
  /**
2814
3567
  * Fail LOUDLY at construction on a misconfigured time policy — a security
@@ -2823,11 +3576,17 @@ var PipRailClient = class {
2823
3576
  if (!policy) return;
2824
3577
  const hasWindowTotal = policy.windowTotal !== void 0;
2825
3578
  const hasWindowSeconds = policy.windowSeconds !== void 0;
2826
- if (hasWindowTotal !== hasWindowSeconds) {
3579
+ const hasWindowCount = policy.maxPaymentsPerWindow !== void 0;
3580
+ if (hasWindowTotal && !hasWindowSeconds) {
2827
3581
  throw new TypeError(
2828
3582
  "policy.windowTotal and policy.windowSeconds must be set together \u2014 a rolling-window cap can't be half-armed (set both, or neither)."
2829
3583
  );
2830
3584
  }
3585
+ if (hasWindowSeconds && !hasWindowTotal && !hasWindowCount) {
3586
+ throw new TypeError(
3587
+ "policy.windowSeconds must be set together with policy.windowTotal and/or policy.maxPaymentsPerWindow \u2014 a window width alone bounds nothing."
3588
+ );
3589
+ }
2831
3590
  if (hasWindowSeconds && !(Number.isSafeInteger(policy.windowSeconds) && policy.windowSeconds > 0)) {
2832
3591
  throw new TypeError("policy.windowSeconds must be a positive integer number of seconds.");
2833
3592
  }
@@ -2839,6 +3598,14 @@ var PipRailClient = class {
2839
3598
  );
2840
3599
  }
2841
3600
  }
3601
+ if (policy.expiresAt !== void 0) {
3602
+ const at = policy.expiresAt;
3603
+ if (!Number.isSafeInteger(at) || Math.abs(at) > 864e13) {
3604
+ throw new TypeError(
3605
+ `policy.expiresAt must be an absolute epoch-MILLISECONDS integer (like Date.now()); got ${JSON.stringify(at)}.`
3606
+ );
3607
+ }
3608
+ }
2842
3609
  }
2843
3610
  /** Emit an observability event, never letting a throwing handler break the
2844
3611
  * payment flow (mirrors the server gate's `onPaid` isolation). */
@@ -2848,6 +3615,129 @@ var PipRailClient = class {
2848
3615
  } catch {
2849
3616
  }
2850
3617
  }
3618
+ /**
3619
+ * Capture the verifiable receipt from a settled response (pure — no chain read), stamping
3620
+ * the resource URL this client actually fetched (authoritative over the gate's default ''). A
3621
+ * settled fetch with no receipt extension sets it to `null` so {@link lastReceipt} reflects the
3622
+ * latest fetch. Never throws — a malformed header just yields `null`.
3623
+ */
3624
+ captureReceipt(response, url) {
3625
+ try {
3626
+ const parsed = parseReceiptExtension(response);
3627
+ this.lastReceiptValue = parsed ? { ...parsed, resource: { url } } : null;
3628
+ } catch {
3629
+ this.lastReceiptValue = null;
3630
+ }
3631
+ }
3632
+ /**
3633
+ * The verifiable {@link PipRailReceipt} from the most recent settled `fetch` — the
3634
+ * self-contained record the buyer KEEPS and anyone re-verifies against the chain
3635
+ * (see {@link PipRailClient.verifyReceipt}). `null` when the last settled fetch carried
3636
+ * no receipt (the gate's `receipts` option was off) or no payment has settled yet. Pure.
3637
+ */
3638
+ lastReceipt() {
3639
+ return this.lastReceiptValue;
3640
+ }
3641
+ /**
3642
+ * Re-verify ANY {@link PipRailReceipt} against the chain — the anyone-can-run primitive.
3643
+ * Re-reads `receipt.transaction` via the receipt's own network driver and re-derives
3644
+ * `payTo`/`asset`/`payer` from the tx, **never trusting the receipt's claims**: a forged
3645
+ * `payTo`/`asset`/over-stated `amount` makes the driver's `verify()` fail (`ok:false`); a
3646
+ * forged `payer` surfaces as `matchesClaims:false`. Static + WALLET-FREE — a third party
3647
+ * verifies with only a chain + RPC, no PipRail account. **Never throws** (an RPC error or a
3648
+ * malformed receipt → `{ ok:false, error }`). Viem-free here — the chain read happens inside
3649
+ * the lazily-mounted family driver (the protocol layer pulls no chain libs). Durable for
3650
+ * digest-bound families; recency-bounded for the account-watch families (see
3651
+ * {@link ReceiptVerification}).
3652
+ */
3653
+ static async verifyReceipt(receipt, opts) {
3654
+ const r = receipt?.receipt;
3655
+ if (!r || typeof r !== "object") {
3656
+ return { ok: false, onChain: { payTo: "", asset: "", amount: "", payer: "" }, matchesClaims: false, ageSeconds: 0, error: "tx_not_found" };
3657
+ }
3658
+ const claimed = { payTo: r.payTo ?? "", asset: r.asset ?? "", amount: r.amount ?? "", payer: r.payer ?? "" };
3659
+ const ageSeconds = receiptAgeSeconds(r.verifiedAt);
3660
+ try {
3661
+ const chain = chainSelectorForNetwork(r.network, opts?.rpcUrl);
3662
+ const net = await resolveNetwork2({
3663
+ chain,
3664
+ ...opts?.rpcUrl ? { rpcUrl: opts.rpcUrl } : {}
3665
+ });
3666
+ const accept = {
3667
+ scheme: "onchain-proof",
3668
+ network: r.network,
3669
+ amount: r.amount,
3670
+ asset: r.asset,
3671
+ payTo: r.payTo,
3672
+ maxTimeoutSeconds: RECEIPT_VERIFY_WINDOW_SECONDS,
3673
+ extra: {
3674
+ nonce: r.nonce ?? "",
3675
+ // the challenge/memo nonce — REQUIRED for Template-A re-verify
3676
+ decimals: receipt.decimals ?? 6,
3677
+ // B10c — Stellar/XRPL/TON re-scale by it; 6 = USDC fallback
3678
+ minConfirmations: 0,
3679
+ amountFormatted: ""
3680
+ }
3681
+ };
3682
+ const ref = familyForChain(chain) === "near" ? `${r.payer}:${r.transaction}` : r.transaction;
3683
+ const result = await net.verify(ref, accept);
3684
+ if (!result.ok) {
3685
+ return { ok: false, onChain: claimed, matchesClaims: false, ageSeconds, error: result.error };
3686
+ }
3687
+ const oc = result.receipt;
3688
+ const onChain = { payTo: oc.payTo, asset: oc.asset, amount: oc.amount, payer: oc.payer };
3689
+ const matchesClaims = sameAddress(oc.payer, r.payer);
3690
+ return { ok: true, onChain, matchesClaims, ageSeconds };
3691
+ } catch {
3692
+ return { ok: false, onChain: claimed, matchesClaims: false, ageSeconds, error: "tx_not_found" };
3693
+ }
3694
+ }
3695
+ /**
3696
+ * Verify the OPTIONAL Tier-2 service-delivery attestation on a {@link PipRailReceipt}
3697
+ * — the merchant's signed proof that the resource was actually SERVED (the one thing
3698
+ * the chain can't attest). For an EVM EIP-712 attestation this re-recovers the signer
3699
+ * from the signature over the official `offer-receipt` typed data and checks
3700
+ * `recover === receipt.payTo` (spec §4.5.1 / §5.5) — the classic EIP-712 footgun made
3701
+ * safe (`recoverTypedDataAddress` returns a WRONG address rather than throwing on a bad
3702
+ * signature, so the equality check is the real verification). A tampered signature →
3703
+ * `{ ok:false }`, never a throw.
3704
+ *
3705
+ * Static + wallet-free. **Never throws** (a malformed/absent attestation, an unsupported
3706
+ * format, or a recovery fault → `{ ok:false, reason }`). Viem-free HERE — the recover runs
3707
+ * inside the lazily-imported EVM receipt driver (a lazy chunk), so the protocol layer pulls
3708
+ * no chain libs. The JWS format defers to R3 (`{ ok:false, reason:'jws-not-loaded' }`).
3709
+ */
3710
+ static async verifyAttestation(receipt) {
3711
+ const att = receipt?.attestation;
3712
+ if (!att || typeof att !== "object" || typeof att.signature !== "string") {
3713
+ return { ok: false, reason: "no-attestation" };
3714
+ }
3715
+ if (att.format === "jws") {
3716
+ return { ok: false, reason: "jws-not-loaded" };
3717
+ }
3718
+ const r = receipt.receipt;
3719
+ if (!r || typeof r !== "object") return { ok: false, reason: "no-receipt" };
3720
+ const payload = att.payload ?? {};
3721
+ const network = typeof payload.network === "string" ? payload.network : r.network;
3722
+ const resourceUrl = typeof payload.resourceUrl === "string" ? payload.resourceUrl : receipt.resource?.url ?? "";
3723
+ const payer = typeof payload.payer === "string" ? payload.payer : r.payer;
3724
+ const issuedAt = typeof payload.issuedAt === "number" ? payload.issuedAt : receiptIssuedAtSeconds(r.verifiedAt);
3725
+ const transaction = typeof payload.transaction === "string" ? payload.transaction : r.transaction ?? "";
3726
+ try {
3727
+ const { verifyReceiptAttestationEvm } = await import("./receipt-NNEID77X.js");
3728
+ return await verifyReceiptAttestationEvm({
3729
+ payTo: r.payTo,
3730
+ network,
3731
+ resourceUrl,
3732
+ payer,
3733
+ issuedAt,
3734
+ transaction,
3735
+ signature: att.signature
3736
+ });
3737
+ } catch {
3738
+ return { ok: false, reason: "verify-failed" };
3739
+ }
3740
+ }
2851
3741
  /** Auto-mount the chain's driver, resolve the network, and bind the wallet — once. */
2852
3742
  ensure() {
2853
3743
  return this.bound ??= (async () => {
@@ -2927,20 +3817,35 @@ var PipRailClient = class {
2927
3817
  async estimateCost(url, init) {
2928
3818
  const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
2929
3819
  if (res.status !== 402) return null;
2930
- const { net, accept, quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
2931
- const cost = await net.estimateCost(accept);
2932
- return { quote, cost };
3820
+ try {
3821
+ const { net, accept, quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
3822
+ const cost = await net.estimateCost(accept);
3823
+ return { quote, cost };
3824
+ } catch (err) {
3825
+ if (err instanceof InvalidEnvelopeError) return null;
3826
+ throw err;
3827
+ }
2933
3828
  }
2934
3829
  /** Aggregated snapshot of every payment this client has settled — total
2935
- * count, cumulative spend per token, and the individual records. */
3830
+ * count, cumulative spend per token, cumulative spend per denomination (the
3831
+ * cross-token grand total), and the individual records. */
2936
3832
  spent() {
2937
3833
  return this.ledger.summary();
2938
3834
  }
3835
+ /** The CONFIGURED spend policy, read back unchanged — so an agent can self-check
3836
+ * its WHOLE leash (caps, allowlists, time, denom + count limits) without hitting a
3837
+ * decline. `undefined` when no policy is set. Pure; never throws. */
3838
+ policy() {
3839
+ return this.opts.policy;
3840
+ }
2939
3841
  /**
2940
3842
  * Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
2941
3843
  * the consent, and this is how the agent SEES what's left of it before paying.
2942
- * Composes the in-memory ledger with the configured policy; never throws, moves
2943
- * no funds. PROCESS-SCOPED every figure resets on restart (see {@link SessionBudget}).
3844
+ * Composes the ledger with the configured policy: the per-asset money leash
3845
+ * (`byAsset`), the cross-token GRAND TOTAL per denomination (`byDenom`, present
3846
+ * from the start), and the payment-COUNT leash (`counts`). Never throws, moves no
3847
+ * funds. The money/count figures persist if a `spendStore` is set; the time
3848
+ * envelope is process-scoped (see {@link SessionBudget}).
2944
3849
  */
2945
3850
  budget() {
2946
3851
  const view = this.sessionView();
@@ -2951,9 +3856,54 @@ var PipRailClient = class {
2951
3856
  expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
2952
3857
  secondsRemaining: view?.secondsRemaining ?? null
2953
3858
  },
2954
- byAsset: this.remaining()
3859
+ byAsset: this.remaining(),
3860
+ byDenom: this.denomRemaining(),
3861
+ counts: this.countStatus()
2955
3862
  };
2956
3863
  }
3864
+ /**
3865
+ * The cross-token GRAND-TOTAL leash — one row per denomination capped by
3866
+ * `policy.maxTotalPerDenom`. Unlike `remaining()`, rows exist from the START
3867
+ * (the cap is a single declared number, so headroom is previewable before any
3868
+ * spend). `[]` when no `maxTotalPerDenom` is set. Pure; never throws; never a
3869
+ * price-converted figure (tokens grouped as one unit, each 1:1).
3870
+ */
3871
+ denomRemaining() {
3872
+ const caps = this.opts.policy?.maxTotalPerDenom;
3873
+ if (!caps) return [];
3874
+ return Object.entries(caps).map(([rawDenom, capStr]) => {
3875
+ const denom = rawDenom.toUpperCase();
3876
+ const spentScaled = this.ledger.totalForDenom(denom);
3877
+ const capScaled = floorUnits(capStr, DENOM_PRECISION);
3878
+ const remainingScaled = capScaled > spentScaled ? capScaled - spentScaled : 0n;
3879
+ const fraction = capScaled === 0n ? spentScaled > 0n ? 1 : 0 : Number(spentScaled * 1000000n / capScaled) / 1e6;
3880
+ return {
3881
+ denom,
3882
+ spentFormatted: formatUnits(spentScaled, DENOM_PRECISION),
3883
+ capFormatted: formatUnits(capScaled, DENOM_PRECISION),
3884
+ remainingFormatted: formatUnits(remainingScaled, DENOM_PRECISION),
3885
+ fraction
3886
+ };
3887
+ });
3888
+ }
3889
+ /** The payment-COUNT leash — settled count so far + the configured lifetime/window
3890
+ * caps and what's left. Pure; never throws. */
3891
+ countStatus() {
3892
+ const policy = this.opts.policy;
3893
+ const settled = this.ledger.count();
3894
+ const out = { settled };
3895
+ if (policy?.maxPayments !== void 0) {
3896
+ out.lifetimeCap = policy.maxPayments;
3897
+ out.lifetimeRemaining = Math.max(0, policy.maxPayments - settled);
3898
+ }
3899
+ if (policy?.maxPaymentsPerWindow !== void 0 && policy.windowSeconds !== void 0) {
3900
+ const windowSettled = this.ledger.countSince(Date.now() - policy.windowSeconds * 1e3);
3901
+ out.windowCap = policy.maxPaymentsPerWindow;
3902
+ out.windowSettled = windowSettled;
3903
+ out.windowRemaining = Math.max(0, policy.maxPaymentsPerWindow - windowSettled);
3904
+ }
3905
+ return out;
3906
+ }
2957
3907
  /**
2958
3908
  * Per-(network, asset) remaining budget — ONE row per pair the ledger already
2959
3909
  * holds (decimals are known only after the first spend), so a fresh client with
@@ -3226,9 +4176,17 @@ var PipRailClient = class {
3226
4176
  }
3227
4177
  this.safeEmit({ kind: "payment-required", challenge, accept });
3228
4178
  await this.authorize(quote);
4179
+ if (accept.scheme === "upto") {
4180
+ return this.payUptoRail(net, wallet, accept, url, init, quote);
4181
+ }
3229
4182
  if (accept.scheme === "exact") {
3230
4183
  return this.payExactRail(net, wallet, accept, url, init, quote);
3231
4184
  }
4185
+ if (accept.scheme !== "onchain-proof") {
4186
+ throw new UnsupportedSchemeError(
4187
+ `internal: unrouted accept scheme '${accept.scheme}' reached the onchain-proof pay path.`
4188
+ );
4189
+ }
3232
4190
  const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
3233
4191
  const response = await this.retryWithProof(url, init, accept, ref, confirmed);
3234
4192
  this.recordSpend(quote, ref);
@@ -3315,6 +4273,13 @@ var PipRailClient = class {
3315
4273
  )
3316
4274
  );
3317
4275
  }
4276
+ if (schemes.includes("upto")) {
4277
+ out.push(
4278
+ ...challenge.accepts.filter(
4279
+ (a) => a.scheme === "upto" && this.supportsNetwork(net, a.network) && typeof net.payUpto === "function" && net.describeAsset(a.asset) != null && typeof a.extra?.facilitatorAddress === "string" && a.extra.facilitatorAddress.length > 0 && Number.isInteger(a.maxTimeoutSeconds) && a.maxTimeoutSeconds > 0
4280
+ )
4281
+ );
4282
+ }
3318
4283
  return out;
3319
4284
  }
3320
4285
  /** Build the full {@link PaymentPlan} from an already-parsed challenge + bound
@@ -3336,11 +4301,11 @@ var PipRailClient = class {
3336
4301
  ...session ? { session } : {}
3337
4302
  };
3338
4303
  }
3339
- const analysed = await Promise.all(
4304
+ const analysed = (await Promise.all(
3340
4305
  candidates.map(
3341
- (accept) => this.analyzeRail(net, wallet, accept, url, challenge.resource.description)
4306
+ (accept) => this.analyzeRail(net, wallet, accept, url, challenge.resource.description).catch(() => null)
3342
4307
  )
3343
- );
4308
+ )).filter((o) => o !== null);
3344
4309
  const options = rankOptions(analysed);
3345
4310
  const best = options.find((o) => o.state === "payable") ?? null;
3346
4311
  const status = best ? "ready" : options.some((o) => o.state === "unknown") ? "unknown" : "blocked";
@@ -3364,14 +4329,14 @@ var PipRailClient = class {
3364
4329
  const rr = await net.recipientReady(accept.payTo, accept.asset).catch(() => ({ ready: "unknown" }));
3365
4330
  const amount = BigInt(accept.amount);
3366
4331
  const fee = safeBig(cost.fee);
3367
- const isExact = accept.scheme === "exact";
4332
+ const isExact = accept.scheme === "exact" || accept.scheme === "upto";
3368
4333
  const isNative = accept.asset === "native";
3369
4334
  const blockers = [];
3370
4335
  const warnings = [];
3371
4336
  const shortfall = {};
3372
4337
  if (!quote.withinPolicy) {
3373
4338
  blockers.push(
3374
- quote.policyCode === "SESSION_EXPIRED" || quote.policyCode === "WINDOW_TOTAL" ? "OUTSIDE_WINDOW" : "OUTSIDE_POLICY"
4339
+ quote.policyCode === "SESSION_EXPIRED" || quote.policyCode === "WINDOW_TOTAL" || quote.policyCode === "WINDOW_COUNT" ? "OUTSIDE_WINDOW" : "OUTSIDE_POLICY"
3375
4340
  );
3376
4341
  }
3377
4342
  if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
@@ -3450,6 +4415,11 @@ var PipRailClient = class {
3450
4415
  `challenge for ${accept.asset} on ${accept.network} states no valid decimals and the SDK doesn't recognise the token \u2014 refusing to price it.`
3451
4416
  );
3452
4417
  }
4418
+ if (decimals > MAX_DECIMALS) {
4419
+ throw new InvalidEnvelopeError(
4420
+ `challenge for ${accept.asset} on ${accept.network} states ${decimals} decimals (> ${MAX_DECIMALS}) \u2014 refusing to price it (no real token is that deep).`
4421
+ );
4422
+ }
3453
4423
  const symbol = described?.symbol ?? accept.extra?.symbol;
3454
4424
  const amountFormatted = formatUnits(amountBase, decimals);
3455
4425
  const intent = {
@@ -3464,9 +4434,14 @@ var PipRailClient = class {
3464
4434
  };
3465
4435
  const policy = this.opts.policy;
3466
4436
  const hasWindow = !!policy && policy.windowTotal != null && policy.windowSeconds != null;
4437
+ const hasWindowCount = !!policy && policy.maxPaymentsPerWindow != null && policy.windowSeconds != null;
4438
+ const hasDenomCap = !!policy && policy.maxTotalPerDenom != null && Object.keys(policy.maxTotalPerDenom).length > 0;
4439
+ const hasCountCap = !!policy && policy.maxPayments != null;
3467
4440
  const hasTimePolicy = !!policy && (policy.ttlSeconds != null || policy.expiresAt != null || hasWindow);
4441
+ const needsCtx = hasTimePolicy || hasWindowCount || hasDenomCap || hasCountCap;
4442
+ const denom = hasDenomCap ? denomOf(intent.symbol, intent.asset, policy) : void 0;
3468
4443
  const now = Date.now();
3469
- const ctx = hasTimePolicy ? {
4444
+ const ctx = needsCtx ? {
3470
4445
  now,
3471
4446
  sessionStart: this.ledger.sessionStart,
3472
4447
  // Window slice ONLY when BOTH fields are set — never a `?? 0` width.
@@ -3474,7 +4449,12 @@ var PipRailClient = class {
3474
4449
  accept.network,
3475
4450
  accept.asset,
3476
4451
  now - policy.windowSeconds * 1e3
3477
- ) : 0n
4452
+ ) : 0n,
4453
+ // Running grand total on this intent's denomination, across the (shared) ledger.
4454
+ spentInDenomScaled: denom ? this.ledger.totalForDenom(denom) : 0n,
4455
+ // Settled-payment counts (lifetime + window) for the count caps.
4456
+ paymentCount: hasCountCap ? this.ledger.count() : 0,
4457
+ paymentCountInWindow: hasWindowCount ? this.ledger.countSince(now - policy.windowSeconds * 1e3) : 0
3478
4458
  } : void 0;
3479
4459
  const decision = evaluatePolicy(
3480
4460
  intent,
@@ -3510,9 +4490,11 @@ var PipRailClient = class {
3510
4490
  async authorize(quote) {
3511
4491
  if (!quote.withinPolicy) {
3512
4492
  const reason = `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`;
3513
- const reasonCode = reasonCodeForPolicy(quote.policyCode);
3514
- this.safeEmit({ kind: "payment-failed", reason, code: reasonCode });
3515
- throw new PaymentDeclinedError(reason, { reasonCode });
4493
+ this.refuse(reason, {
4494
+ reasonCode: reasonCodeForPolicy(quote.policyCode),
4495
+ policyCode: quote.policyCode,
4496
+ quote
4497
+ });
3516
4498
  }
3517
4499
  const hook = this.opts.onBeforePay;
3518
4500
  if (!hook) return;
@@ -3520,33 +4502,151 @@ var PipRailClient = class {
3520
4502
  try {
3521
4503
  approved = await hook(quote);
3522
4504
  } catch (err) {
3523
- const reason = "onBeforePay threw \u2014 refusing to pay.";
3524
- this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3525
- throw new PaymentDeclinedError(reason, { cause: err, reasonCode: "APPROVAL" });
4505
+ this.refuse("onBeforePay threw \u2014 refusing to pay.", { reasonCode: "APPROVAL", quote, cause: err });
3526
4506
  }
3527
4507
  if (!approved) {
3528
4508
  const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
3529
- this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3530
- throw new PaymentDeclinedError(reason, { reasonCode: "APPROVAL" });
4509
+ this.refuse(reason, { reasonCode: "APPROVAL", quote });
4510
+ }
4511
+ }
4512
+ /**
4513
+ * Refuse a payment BEFORE any send: emit BOTH the legacy `payment-failed` (so existing
4514
+ * `onEvent` consumers are unaffected) AND the richer, dedicated `payment-declined`
4515
+ * (typed reasonCode + fine PolicyDenyCode + the quote + a budget snapshot), then throw
4516
+ * the typed {@link PaymentDeclinedError}. Returns `never` so callers' control flow is
4517
+ * exhaustive.
4518
+ */
4519
+ refuse(reason, opts) {
4520
+ this.safeEmit({ kind: "payment-failed", reason, ...opts.reasonCode ? { code: opts.reasonCode } : {} });
4521
+ this.safeEmit({
4522
+ kind: "payment-declined",
4523
+ reason,
4524
+ ...opts.reasonCode ? { reasonCode: opts.reasonCode } : {},
4525
+ ...opts.policyCode ? { code: opts.policyCode } : {},
4526
+ ...opts.quote ? { quote: opts.quote } : {},
4527
+ budget: this.budget()
4528
+ });
4529
+ throw new PaymentDeclinedError(reason, {
4530
+ ...opts.reasonCode ? { reasonCode: opts.reasonCode } : {},
4531
+ ...opts.cause !== void 0 ? { cause: opts.cause } : {}
4532
+ });
4533
+ }
4534
+ /** Record a settled payment in the ledger (TRUE decimals for the running total + the
4535
+ * denomination it counts toward in the grand total). Then fire the `onSpend` callback
4536
+ * with the record + the post-payment budget, and emit any `warnAtFraction` thresholds
4537
+ * this payment just crossed. All observability is isolated — a throwing hook never
4538
+ * affects the (already-settled) payment.
4539
+ *
4540
+ * `settledAmountBase` is the SINGLE upto ledger-reconciliation seam: the quote (and thus
4541
+ * the policy/budget) gates on the MAX, and for the metered `upto` rail the budgeted amount
4542
+ * RECORDED is ALSO the authorized MAX — the only buyer-provable bound. The merchant's
4543
+ * claimed actual is UNTRUSTED (a malicious merchant can settle the MAX on-chain yet report
4544
+ * a tiny `SettleOutcome.amount`); recording it would let an under-report silently loosen a
4545
+ * cumulative cap (`maxTotal`/`maxTotalPerDenom`/`windowTotal`) past the buyer's real on-chain
4546
+ * spend (POL-1). So the cap-bearing `amountBase` is the MAX; the clamped actual is surfaced
4547
+ * separately on `settledBase`/`settledFormatted` for transparency (it equals the receipt's
4548
+ * amount). When absent (onchain-proof/exact) this is byte-identical to before. */
4549
+ recordSpend(quote, ref, settledAmountBase) {
4550
+ const denom = denomOf(quote.symbol, quote.asset, this.opts.policy);
4551
+ const amountBase = quote.amount;
4552
+ const amountFormatted = quote.amountFormatted;
4553
+ let settledBase;
4554
+ let settledFormatted;
4555
+ if (settledAmountBase !== void 0 && /^\d+$/.test(settledAmountBase)) {
4556
+ try {
4557
+ const claimed = BigInt(settledAmountBase);
4558
+ const max = BigInt(quote.amount);
4559
+ const shown = claimed < max ? claimed : max;
4560
+ settledBase = shown.toString();
4561
+ settledFormatted = formatUnits(shown, quote.decimals);
4562
+ } catch {
4563
+ }
4564
+ }
4565
+ const record = {
4566
+ url: quote.url,
4567
+ host: hostOf2(quote.url),
4568
+ network: quote.network,
4569
+ asset: quote.asset,
4570
+ amountBase,
4571
+ amountFormatted,
4572
+ ...settledBase !== void 0 ? { settledBase, settledFormatted } : {},
4573
+ ...quote.symbol ? { symbol: quote.symbol } : {},
4574
+ decimals: quote.decimals,
4575
+ ...denom ? { denom } : {},
4576
+ ref,
4577
+ at: (/* @__PURE__ */ new Date()).toISOString()
4578
+ };
4579
+ this.ledger.record(record, quote.decimals, denom);
4580
+ const budget = this.budget();
4581
+ if (this.opts.onSpend) {
4582
+ try {
4583
+ this.opts.onSpend(record, budget);
4584
+ } catch {
4585
+ }
4586
+ }
4587
+ this.emitThresholds(budget);
4588
+ }
4589
+ /**
4590
+ * Emit a `budget-threshold` event for each cap whose used-fraction just reached
4591
+ * `policy.warnAtFraction` — the early warning before a hard decline. Fires ONCE per
4592
+ * crossing per cap (deduped on the shared ledger via `markWarned`, so a cross-chain
4593
+ * threshold fires once for the whole budget, not once per chain). No-op when no
4594
+ * `warnAtFraction` is set. Reads the just-computed {@link SessionBudget}; isolated (safeEmit).
4595
+ */
4596
+ emitThresholds(budget) {
4597
+ const frac = this.opts.policy?.warnAtFraction;
4598
+ if (frac === void 0) return;
4599
+ const fire = (scope, label, spentFormatted, capFormatted, fraction) => {
4600
+ if (fraction < frac) return;
4601
+ if (!this.ledger.markWarned(`${scope}:${label}`)) return;
4602
+ this.safeEmit({ kind: "budget-threshold", scope, label, spentFormatted, capFormatted, fraction });
4603
+ };
4604
+ for (const r of budget.byAsset) {
4605
+ if (r.capBase === void 0) continue;
4606
+ const cap = BigInt(r.capBase);
4607
+ const spent = BigInt(r.spentBase);
4608
+ const fraction = cap === 0n ? spent > 0n ? 1 : 0 : Number(spent * 1000000n / cap) / 1e6;
4609
+ fire(
4610
+ "asset",
4611
+ `${r.symbol ?? r.asset} on ${r.network}`,
4612
+ formatUnits(spent, r.decimals),
4613
+ formatUnits(cap, r.decimals),
4614
+ fraction
4615
+ );
4616
+ }
4617
+ for (const d of budget.byDenom) {
4618
+ fire("denom", d.denom, d.spentFormatted, d.capFormatted, d.fraction);
4619
+ }
4620
+ const policy = this.opts.policy;
4621
+ if (policy?.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
4622
+ const since = Date.now() - policy.windowSeconds * 1e3;
4623
+ for (const r of budget.byAsset) {
4624
+ const cap = floorUnits(policy.windowTotal, r.decimals);
4625
+ const spent = this.ledger.totalSince(r.network, r.asset, since);
4626
+ const fraction = cap === 0n ? spent > 0n ? 1 : 0 : Number(spent * 1000000n / cap) / 1e6;
4627
+ fire(
4628
+ "window",
4629
+ `${r.symbol ?? r.asset} on ${r.network}`,
4630
+ formatUnits(spent, r.decimals),
4631
+ formatUnits(cap, r.decimals),
4632
+ fraction
4633
+ );
4634
+ }
4635
+ }
4636
+ const c = budget.counts;
4637
+ if (c.lifetimeCap !== void 0 && c.lifetimeCap > 0) {
4638
+ fire("count", "maxPayments", String(c.settled), String(c.lifetimeCap), c.settled / c.lifetimeCap);
4639
+ }
4640
+ if (c.windowCap !== void 0 && c.windowCap > 0 && c.windowSettled !== void 0) {
4641
+ fire(
4642
+ "window-count",
4643
+ "maxPaymentsPerWindow",
4644
+ String(c.windowSettled),
4645
+ String(c.windowCap),
4646
+ c.windowSettled / c.windowCap
4647
+ );
3531
4648
  }
3532
4649
  }
3533
- /** Record a settled payment in the ledger (true decimals for the running total). */
3534
- recordSpend(quote, ref) {
3535
- this.ledger.record(
3536
- {
3537
- url: quote.url,
3538
- host: hostOf2(quote.url),
3539
- network: quote.network,
3540
- asset: quote.asset,
3541
- amountBase: quote.amount,
3542
- amountFormatted: quote.amountFormatted,
3543
- ...quote.symbol ? { symbol: quote.symbol } : {},
3544
- ref,
3545
- at: (/* @__PURE__ */ new Date()).toISOString()
3546
- },
3547
- quote.decimals
3548
- );
3549
- }
3550
4650
  async payAndConfirm(net, wallet, accept) {
3551
4651
  if (!this.supportsNetwork(net, accept.network)) {
3552
4652
  throw new WrongChainError(
@@ -3613,6 +4713,7 @@ var PipRailClient = class {
3613
4713
  }
3614
4714
  if (lastResponse.status !== 402) {
3615
4715
  const receipt = parseReceipt(lastResponse);
4716
+ this.captureReceipt(lastResponse, url);
3616
4717
  this.safeEmit({ kind: "payment-settled", receipt });
3617
4718
  return lastResponse;
3618
4719
  }
@@ -3697,6 +4798,7 @@ var PipRailClient = class {
3697
4798
  }
3698
4799
  if (response.ok && !(settle && settle.success === false)) {
3699
4800
  const receipt = parseReceipt(response);
4801
+ this.captureReceipt(response, url);
3700
4802
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
3701
4803
  const ref = settle?.transaction || receipt?.transaction || `eip3009-nonce:${nonce}`;
3702
4804
  this.recordSpend(quote, ref);
@@ -3721,6 +4823,91 @@ var PipRailClient = class {
3721
4823
  { ref: nonce }
3722
4824
  );
3723
4825
  }
4826
+ /**
4827
+ * The standard `upto` (metered) buyer path — a near-clone of {@link payExactRail} with TWO
4828
+ * deltas: (1) it signs a Permit2-upto authorization for the MAX via `payUpto` + frames it
4829
+ * with `buildUptoSignatureHeader`; (2) it records the ACTUAL settled amount (read off the
4830
+ * SettleResponse's required `amount` field, via `recordSpend(quote, ref, settle.amount)`) in
4831
+ * the ledger — the budget gated on the MAX, the ledger records the ACTUAL. A server that omits
4832
+ * `settle.amount` FAILS SAFE to the MAX (over-counts, never under-counts). The buyer SIGNS, the
4833
+ * merchant self-settles — the buyer never broadcasts.
4834
+ */
4835
+ async payUptoRail(net, wallet, accept, url, init, quote) {
4836
+ if (!net.payUpto) {
4837
+ throw new UnsupportedSchemeError(
4838
+ `the ${net.family} family can't pay a standard 'upto' rail (EVM-Permit2 only today).`
4839
+ );
4840
+ }
4841
+ throwIfAborted(init?.signal);
4842
+ const { payload, accepted, payerFrom, nonce } = await net.payUpto(wallet, accept);
4843
+ const headers = new Headers(init?.headers);
4844
+ headers.set(HEADER_SIGNATURE, buildUptoSignatureHeader({ accepted, payload }));
4845
+ const rejectDefinitive = (why2) => {
4846
+ this.safeEmit({ kind: "payment-failed", reason: `upto: facilitator rejected nonce=${nonce} (${why2})`, code: why2 });
4847
+ throw new MaxRetriesExceededError(
4848
+ `upto: the server rejected the payment (${why2}). Fix the cause, then re-present the SAME signed authorization (nonce=${nonce}) \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
4849
+ { ref: nonce }
4850
+ );
4851
+ };
4852
+ const deadline = Date.now() + Math.max(1, Math.floor(accept.maxTimeoutSeconds / 2)) * 1e3;
4853
+ const maxAttempts = Math.min(this.maxRetries, 3);
4854
+ let lastReason = null;
4855
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
4856
+ if (attempt > 0) {
4857
+ if (Date.now() >= deadline) break;
4858
+ await new Promise((r) => setTimeout(r, Math.min(2e3, 400 * 2 ** (attempt - 1))));
4859
+ }
4860
+ throwIfAborted(init?.signal);
4861
+ const budget = Math.min(this.retryTimeoutMs, deadline - Date.now());
4862
+ if (budget <= 0) break;
4863
+ const timeoutController = new AbortController();
4864
+ const timeoutId = setTimeout(() => timeoutController.abort(), budget);
4865
+ const signal = init?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, init.signal]) : timeoutController.signal;
4866
+ let response;
4867
+ try {
4868
+ response = await fetch(url, { ...init ?? {}, headers, signal });
4869
+ } catch (err) {
4870
+ throw new PaymentTimeoutError(
4871
+ `upto: no response after submitting the authorization (nonce=${nonce}) to ${hostOf2(url)}. The merchant may have already settled it \u2014 verify on-chain before re-presenting; do NOT re-pay.`,
4872
+ { cause: err, ref: nonce }
4873
+ );
4874
+ } finally {
4875
+ clearTimeout(timeoutId);
4876
+ }
4877
+ const settle = parseSettleResponse(response);
4878
+ if (response.status === 402) {
4879
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the server reported success:false");
4880
+ lastReason = await readInvalidReason(response) ?? lastReason;
4881
+ continue;
4882
+ }
4883
+ if (response.ok && !(settle && settle.success === false)) {
4884
+ const receipt = parseReceipt(response);
4885
+ this.captureReceipt(response, url);
4886
+ this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
4887
+ const ref = settle?.transaction || receipt?.transaction || `upto-nonce:${nonce}`;
4888
+ const settledAmount = settle?.amount ?? receipt?.amount;
4889
+ this.recordSpend(quote, ref, settledAmount);
4890
+ return response;
4891
+ }
4892
+ if (response.status >= 500) {
4893
+ this.safeEmit({ kind: "payment-failed", reason: `upto: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
4894
+ return response;
4895
+ }
4896
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the server reported success:false");
4897
+ this.safeEmit({ kind: "payment-failed", reason: `upto: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
4898
+ return response;
4899
+ }
4900
+ const why = lastReason ? `${lastReason.error}${lastReason.detail ? ` \u2014 ${lastReason.detail}` : ""}` : "server gave no reason";
4901
+ this.safeEmit({
4902
+ kind: "payment-failed",
4903
+ reason: `upto: 402 after submitting authorization nonce=${nonce} (${why})`,
4904
+ ...lastReason ? { code: lastReason.error, detail: lastReason.detail } : {}
4905
+ });
4906
+ throw new MaxRetriesExceededError(
4907
+ `upto: server still returned 402 after submitting the signed authorization (nonce=${nonce}). Last rejection: ${why}. Re-present the SAME authorization \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
4908
+ { ref: nonce }
4909
+ );
4910
+ }
3724
4911
  };
3725
4912
  function throwIfAborted(signal) {
3726
4913
  if (signal?.aborted) {
@@ -3843,8 +5030,11 @@ function reasonCodeForPolicy(code) {
3843
5030
  case "SESSION_EXPIRED":
3844
5031
  return "SESSION_EXPIRED";
3845
5032
  case "WINDOW_TOTAL":
5033
+ case "WINDOW_COUNT":
3846
5034
  return "OUTSIDE_WINDOW";
3847
5035
  case "MAX_TOTAL":
5036
+ case "MAX_TOTAL_DENOM":
5037
+ case "MAX_PAYMENTS":
3848
5038
  return "BUDGET";
3849
5039
  case void 0:
3850
5040
  return void 0;
@@ -3898,10 +5088,46 @@ async function readInvalidReason(response) {
3898
5088
  if (settle?.errorReason) return { error: settle.errorReason, detail: "" };
3899
5089
  return null;
3900
5090
  }
5091
+ var EVM_PRESET_FOR_CHAINID = {
5092
+ 1: "ethereum",
5093
+ 8453: "base",
5094
+ 137: "polygon",
5095
+ 42161: "arbitrum",
5096
+ 10: "optimism",
5097
+ 43114: "avalanche",
5098
+ 56: "bnb"
5099
+ };
5100
+ function chainSelectorForNetwork(network, rpcUrl) {
5101
+ const chainId = chainIdFromNetwork(network);
5102
+ if (chainId !== null) {
5103
+ const preset = EVM_PRESET_FOR_CHAINID[chainId];
5104
+ if (preset) return preset;
5105
+ return { id: chainId, rpcUrl: rpcUrl ?? "" };
5106
+ }
5107
+ const namespace = network.split(":")[0] ?? "";
5108
+ return namespace === "tvm" ? "ton" : namespace;
5109
+ }
5110
+ function receiptAgeSeconds(verifiedAt) {
5111
+ const t = Date.parse(verifiedAt);
5112
+ if (Number.isNaN(t)) return 0;
5113
+ return Math.max(0, Math.round((Date.now() - t) / 1e3));
5114
+ }
5115
+ function receiptIssuedAtSeconds(verifiedAt) {
5116
+ const t = Date.parse(verifiedAt);
5117
+ if (Number.isNaN(t)) return 0;
5118
+ return Math.floor(t / 1e3);
5119
+ }
5120
+ function sameAddress(a, b) {
5121
+ return typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
5122
+ }
3901
5123
 
3902
5124
  // src/payer.ts
3903
5125
  var MultiChainPayer = class _MultiChainPayer {
3904
5126
  _clients;
5127
+ /** True when every client shares ONE ledger (the `fromWallets` path) — so spend +
5128
+ * budget are read from that single source, never concatenated (which would
5129
+ * double-count a shared ledger). */
5130
+ _shared;
3905
5131
  /**
3906
5132
  * Wrap an explicit, ordered set of single-chain clients — use this when a client
3907
5133
  * needs full control (e.g. a custom EVM chain configured by a viem `Chain`). The
@@ -3909,12 +5135,18 @@ var MultiChainPayer = class _MultiChainPayer {
3909
5135
  * at MOST one client per chain — two clients on the SAME network would double-count in
3910
5136
  * `spent()`/`budget()` and waste a plan round-trip (`fromWallets` can't produce this).
3911
5137
  * For the common case, prefer {@link MultiChainPayer.fromWallets}.
5138
+ *
5139
+ * For a cross-chain grand total / count cap via this path, build the clients with a
5140
+ * SHARED `ledger` (`new SpendLedger(store?)` passed to each client's `ledger` option)
5141
+ * and set `opts.shared = true` so reads come from that one ledger. `fromWallets` does
5142
+ * this for you.
3912
5143
  */
3913
- constructor(clients) {
5144
+ constructor(clients, opts = {}) {
3914
5145
  if (clients.length === 0) {
3915
5146
  throw new TypeError("MultiChainPayer needs at least one PipRailClient.");
3916
5147
  }
3917
5148
  this._clients = [...clients];
5149
+ this._shared = opts.shared ?? false;
3918
5150
  }
3919
5151
  /**
3920
5152
  * Build one client per funded chain from a `{ chain → wallet }` map — the
@@ -3929,20 +5161,26 @@ var MultiChainPayer = class _MultiChainPayer {
3929
5161
  * solana: { key: process.env.SOLANA_SECRET! },
3930
5162
  * xrpl: { key: process.env.XRPL_SEED! },
3931
5163
  * },
3932
- * policy: { maxAmount: '1.00', maxTotal: '20.00', tokens: ['USDC', 'USDT'] },
5164
+ * // ONE budget across all three chains: $20 total + at most 100 payments.
5165
+ * policy: { maxAmount: '1.00', maxTotalPerDenom: { USD: '20.00' }, maxPayments: 100 },
3933
5166
  * })
3934
5167
  * const res = await payer.get('https://api.example.com/paid') // pays on the first funded chain that can settle
3935
5168
  * ```
5169
+ *
5170
+ * Every client shares ONE {@link SpendLedger} (optionally backed by `spendStore`), so
5171
+ * the cross-token grand total + the count caps span all chains as a single budget.
3936
5172
  */
3937
5173
  static fromWallets(opts) {
3938
5174
  const entries = Object.entries(opts.wallets);
3939
5175
  if (entries.length === 0) {
3940
5176
  throw new TypeError("MultiChainPayer.fromWallets needs at least one wallet.");
3941
5177
  }
5178
+ const ledger = new SpendLedger(opts.spendStore);
3942
5179
  const clients = entries.map(
3943
5180
  ([chain, wallet]) => new PipRailClient({
3944
5181
  chain,
3945
5182
  wallet,
5183
+ ledger,
3946
5184
  ...opts.policy ? { policy: opts.policy } : {},
3947
5185
  ...opts.schemes ? { schemes: opts.schemes } : {},
3948
5186
  ...opts.rpcUrls?.[chain] ? { rpcUrl: opts.rpcUrls[chain] } : {},
@@ -3952,7 +5190,7 @@ var MultiChainPayer = class _MultiChainPayer {
3952
5190
  ...opts.retryTimeoutMs != null ? { retryTimeoutMs: opts.retryTimeoutMs } : {}
3953
5191
  })
3954
5192
  );
3955
- return new _MultiChainPayer(clients);
5193
+ return new _MultiChainPayer(clients, { shared: true });
3956
5194
  }
3957
5195
  /** The underlying single-chain clients, in preference order. Reach for one of
3958
5196
  * these for chain-specific reads (`estimateCost`, `discoverySigner`, per-chain
@@ -4046,28 +5284,50 @@ var MultiChainPayer = class _MultiChainPayer {
4046
5284
  register(url, opts = {}) {
4047
5285
  return this._clients[0].register(url, opts);
4048
5286
  }
4049
- /** Aggregate spend across every chain — counts summed; per-(network,asset) rows
4050
- * and records concatenated (no cross-chain collisions, never a cross-token sum). */
5287
+ /**
5288
+ * Aggregate spend across every chain. With a SHARED ledger (`fromWallets`), this reads
5289
+ * that single ledger ONCE (its records already span every chain) — so the count and the
5290
+ * cross-token `byDenom` grand total are correct and never double-counted. With
5291
+ * independent clients (the explicit constructor), it concatenates: counts summed,
5292
+ * per-(network,asset) `byAsset` + `byDenom` + records concatenated.
5293
+ */
4051
5294
  spent() {
5295
+ if (this._shared) return this._clients[0].spent();
4052
5296
  const summaries = this._clients.map((c) => c.spent());
4053
5297
  return {
4054
5298
  count: summaries.reduce((n, s) => n + s.count, 0),
4055
5299
  byAsset: summaries.flatMap((s) => s.byAsset),
5300
+ byDenom: summaries.flatMap((s) => s.byDenom),
4056
5301
  records: summaries.flatMap((s) => s.records)
4057
5302
  };
4058
5303
  }
4059
- /** A merged budget view: every chain's per-(network,asset) remaining rows, plus the
4060
- * MOST-RESTRICTIVE session time envelope across chains (the soonest deadline wins).
4061
- * Mirrors {@link PipRailClient.budget}'s shape so the agent toolkit reads it
4062
- * unchanged; per-chain session detail is on each `clients[i].budget()`. */
5304
+ /**
5305
+ * A merged budget view. With a SHARED ledger (`fromWallets`) it's the single source of
5306
+ * truth `byAsset`/`byDenom`/`counts` already span every chain, so the grand-total and
5307
+ * count leashes read as ONE budget. With independent clients it merges: per-(network,
5308
+ * asset) rows + per-denom rows concatenated, counts summed, and the MOST-RESTRICTIVE
5309
+ * session time envelope (the soonest deadline wins). Per-chain detail is on each
5310
+ * `clients[i].budget()`.
5311
+ */
4063
5312
  budget() {
5313
+ if (this._shared) return this._clients[0].budget();
4064
5314
  const budgets = this._clients.map((c) => c.budget());
4065
5315
  const session = budgets.map((b) => b.session).reduce((soonest, s) => {
4066
5316
  if (soonest.secondsRemaining == null) return s;
4067
5317
  if (s.secondsRemaining == null) return soonest;
4068
5318
  return s.secondsRemaining < soonest.secondsRemaining ? s : soonest;
4069
5319
  });
4070
- return { session, byAsset: budgets.flatMap((b) => b.byAsset) };
5320
+ return {
5321
+ session,
5322
+ byAsset: budgets.flatMap((b) => b.byAsset),
5323
+ byDenom: budgets.flatMap((b) => b.byDenom),
5324
+ counts: { settled: budgets.reduce((n, b) => n + b.counts.settled, 0) }
5325
+ };
5326
+ }
5327
+ /** The CONFIGURED spend policy (the shared policy `fromWallets` applies to every chain;
5328
+ * the first client's for the explicit constructor). `undefined` when none is set. */
5329
+ policy() {
5330
+ return this._clients[0].policy();
4071
5331
  }
4072
5332
  };
4073
5333
  function isBodyInit(value) {
@@ -4092,7 +5352,13 @@ var BRAND = {
4092
5352
  };
4093
5353
  var WHAT = 'This is an x402 "402 Payment Required" endpoint. Pay one of the offered rails to access it.';
4094
5354
  function howFor(scheme) {
4095
- return scheme === "exact" ? "Standard x402 exact rail \u2014 sign an EIP-3009 / Permit2 / SVM authorization; any stock x402 client (e.g. @x402/fetch) can pay this." : "Pay this amount on-chain to payTo, then resubmit with a payment-signature header carrying the proof ref + nonce. Easiest with @piprail/sdk (see sdk.install).";
5355
+ if (scheme === "exact") {
5356
+ return "Standard x402 exact rail \u2014 sign an EIP-3009 / Permit2 / SVM authorization; any stock x402 client (e.g. @x402/fetch) can pay this.";
5357
+ }
5358
+ if (scheme === "upto") {
5359
+ return "Standard x402 upto (metered) rail \u2014 sign a Permit2 authorization for the MAX amount; the server settles the ACTUAL (\u2264 max) after serving. BUDGET AGAINST THE MAX \u2014 the server MAY fully charge it. EVM-Permit2 only; enable with schemes:['upto'].";
5360
+ }
5361
+ return "Pay this amount on-chain to payTo, then resubmit with a payment-signature header carrying the proof ref + nonce. Easiest with @piprail/sdk (see sdk.install).";
4096
5362
  }
4097
5363
  function railOf(a) {
4098
5364
  const extra = a.extra ?? {};
@@ -4119,6 +5385,7 @@ function buildSelfDescription(input) {
4119
5385
  mcp: { run: BRAND.mcpRun, tool: "piprail_pay_request" },
4120
5386
  docs: { home: BRAND.home, agents: BRAND.docs, pay: BRAND.payDocs },
4121
5387
  discovery: { openapi: "/openapi.json", wellKnown: "/.well-known/x402" },
5388
+ ...input.verifiableReceipts ? { verifiableReceipts: true } : {},
4122
5389
  ...input.instruction ? { instruction: input.instruction } : {}
4123
5390
  };
4124
5391
  }
@@ -4172,9 +5439,12 @@ function explainDecline(err) {
4172
5439
  }
4173
5440
  function formatSpendReport(summary) {
4174
5441
  if (summary.count === 0) return "No payments yet.";
4175
- return summary.byAsset.map(
5442
+ const perAsset = summary.byAsset.map(
4176
5443
  (a) => `${a.totalFormatted} ${a.symbol ?? a.asset} on ${a.network} (${a.count} payment${a.count === 1 ? "" : "s"})`
4177
5444
  ).join("; ");
5445
+ if (summary.byDenom.length === 0) return perAsset;
5446
+ const grand = summary.byDenom.map((d) => `${d.totalFormatted} ${d.denom} total`).join("; ");
5447
+ return `${perAsset} \u2014 grand total: ${grand}`;
4178
5448
  }
4179
5449
  function describeChallenge(challenge) {
4180
5450
  const first = challenge.accepts[0];
@@ -4215,17 +5485,21 @@ and pay with the tools below.
4215
5485
  Always plan before you pay so you never commit to a payment you cannot finish.
4216
5486
 
4217
5487
  ## Gasless \u2014 the exact rail (zero gas for you)
4218
- A 402 may offer up to two rails; you don't choose per payment \u2014 the client does, automatically:
5488
+ A 402 may offer up to three rails; you don't choose per payment \u2014 the client does, automatically:
4219
5489
  - onchain-proof (PipRail's default): you broadcast the payment yourself and pay the network gas
4220
5490
  (the native coin \u2014 ETH/SOL/\u2026). Works on every chain.
4221
5491
  - exact (the ratified x402 rail, opt-in): you only SIGN; the server \u2014 or a facilitator it chose
4222
5492
  (e.g. PayAI) \u2014 broadcasts it, so you pay ZERO gas (you need only the token, no native coin). It
4223
5493
  works on EVM, Solana + Algorand, and the on-chain method (EIP-3009 / Permit2 / SVM / Algorand
4224
5494
  fee-pooled group) is picked automatically.
5495
+ - upto (the metered/variable x402 rail, opt-in, EVM): the amount you see is a MAXIMUM \u2014 you sign
5496
+ a ceiling, the server meters real usage and settles the ACTUAL (<= the max). BUDGET AGAINST THE MAX:
5497
+ the plan/policy treat the ceiling as the spend (a server may charge up to it), so a payable plan
5498
+ means the MAX fits your budget; the settled actual is recorded for reconciliation.
4225
5499
  When the exact scheme is enabled AND balance-aware routing is on, paying picks the cheapest
4226
5500
  settleable rail \u2014 i.e. the gasless exact one. Nothing changes in your loop: quote \u2192 plan \u2192 pay is
4227
- identical. The exact scheme is OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact);
4228
- you can't enable it yourself, but you can report when a 402 needs it (see UNSUPPORTED_SCHEME below).
5501
+ identical. The exact/upto schemes are OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact,upto);
5502
+ you can't enable them yourself, but you can report when a 402 needs one (see UNSUPPORTED_SCHEME below).
4229
5503
 
4230
5504
  ## Reading a refusal \u2014 never crash, never double-spend
4231
5505
  A failed pay returns a STRUCTURED object, never a thrown error you must catch:
@@ -4253,9 +5527,11 @@ Branch on \`code\` (always reliable). Key cases:
4253
5527
  once (enable the exact scheme); report it, don't retry the same call blindly.
4254
5528
 
4255
5529
  ## Knowing your leash \u2014 call piprail_budget
4256
- piprail_budget tells you how much budget and time you have left, per
4257
- (network, asset), plus your spend so far. Read-only; moves no funds. Use it in
4258
- Mode A to self-check before paying.
5530
+ piprail_budget tells you how much budget and time you have left: per (network,
5531
+ asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much
5532
+ USD you can still spend across every stablecoin and chain), the payment-count
5533
+ leash, the session time envelope, your spend so far, and the configured policy
5534
+ read back. Read-only; moves no funds. Use it in Mode A to self-check before paying.
4259
5535
 
4260
5536
  ## Two modes
4261
5537
  - Mode A (headless, default): you run FREE inside a pre-set budget + time
@@ -4266,10 +5542,15 @@ Mode A to self-check before paying.
4266
5542
  do NOT retry it as if it were a transient error.
4267
5543
 
4268
5544
  ## Hard facts
4269
- - Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014
4270
- budgets aren't summed across tokens (no price oracle).
4271
- - Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart
4272
- (a convenience, not a durable ledger).
5545
+ - Per-payment + per-(network, asset) caps always apply. A cross-token GRAND TOTAL per
5546
+ denomination (maxTotalPerDenom, e.g. "$20 across every USD stablecoin + chain") is
5547
+ OPTIONAL \u2014 it sums tokens declared as one unit, each 1:1; it is NOT a price oracle and
5548
+ never prices a volatile native coin. Payment-COUNT caps (maxPayments / per-window) also
5549
+ span every chain + token.
5550
+ - The time envelope lives IN-MEMORY for THIS process (resets on restart). The money + count
5551
+ totals also reset on restart UNLESS a durable spend store is configured \u2014 then they resume.
5552
+ - A refusal arrives as declined:true with a reasonCode; 'BUDGET' covers the lifetime, denom,
5553
+ and count caps; 'OUTSIDE_WINDOW' covers both the rolling money and rolling count windows.
4273
5554
  `;
4274
5555
  function agentGuide() {
4275
5556
  return PIPRAIL_AGENT_GUIDE;
@@ -4501,11 +5782,13 @@ function paymentTools(client) {
4501
5782
  }
4502
5783
  res = await client.fetch(url, { method, headers, body });
4503
5784
  }
5785
+ const verifiable = parseReceiptExtension(res);
4504
5786
  return {
4505
5787
  status: res.status,
4506
5788
  ok: res.ok,
4507
5789
  body: await readBody(res),
4508
- receipt: parseReceipt(res)
5790
+ receipt: parseReceipt(res),
5791
+ ...verifiable ? { verifiableReceipt: { ...verifiable, resource: { url } } } : {}
4509
5792
  };
4510
5793
  } catch (err) {
4511
5794
  if (err instanceof PipRailError) {
@@ -4584,11 +5867,11 @@ function paymentTools(client) {
4584
5867
  },
4585
5868
  {
4586
5869
  name: "piprail_budget",
4587
- description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: totals and the time envelope are in-memory for THIS process and reset on restart.",
5870
+ description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much USD you can still spend across every stablecoin and chain), the payment-count leash, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: the time envelope is in-memory for THIS process; the money/count totals persist only if a spend store is configured.",
4588
5871
  annotations: {
4589
5872
  title: "Check remaining budget",
4590
5873
  readOnlyHint: true,
4591
- // reads the in-memory ledger + policy; never pays
5874
+ // reads the ledger + policy; never pays
4592
5875
  idempotentHint: true
4593
5876
  // a pure read
4594
5877
  },
@@ -4601,7 +5884,13 @@ function paymentTools(client) {
4601
5884
  return {
4602
5885
  spent,
4603
5886
  remaining: budget.byAsset,
5887
+ grandTotal: budget.byDenom,
5888
+ // cross-token spend cap per denomination (USD/EUR/…)
5889
+ counts: budget.counts,
5890
+ // payment-count leash (settled + lifetime/window caps)
4604
5891
  session: budget.session,
5892
+ policy: client.policy() ?? null,
5893
+ // the configured leash, read back
4605
5894
  report: formatSpendReport(spent)
4606
5895
  };
4607
5896
  } catch (err) {
@@ -4619,6 +5908,38 @@ function paymentTools(client) {
4619
5908
  },
4620
5909
  parameters: { type: "object", properties: {}, additionalProperties: false },
4621
5910
  invoke: async () => ({ guide: PIPRAIL_AGENT_GUIDE })
5911
+ },
5912
+ {
5913
+ name: "piprail_verify_receipt",
5914
+ description: "Re-verify a PipRail VERIFIABLE RECEIPT against the chain \u2014 confirm a payment REALLY settled (the funds provably moved to payTo for AT LEAST the stated amount) WITHOUT trusting whoever handed you the receipt. Read-only and WALLET-FREE: pass the PipRailReceipt JSON (from a prior piprail_pay_request `verifiableReceipt`, or any third party). Returns { ok, onChain:{payTo,asset,amount,payer}, matchesClaims, ageSeconds, error? }: `ok` = the chain confirms the settlement; `onChain.payer` is RE-DERIVED from the tx and `matchesClaims:false` means the receipt forged the payer; `amount` is a verified lower bound. Pass `rpcUrl` for a chain outside the common presets.",
5915
+ annotations: {
5916
+ title: "Verify a payment receipt",
5917
+ readOnlyHint: true,
5918
+ // re-reads the chain; moves nothing, needs no wallet
5919
+ idempotentHint: true,
5920
+ openWorldHint: true
5921
+ // reads an on-chain tx via RPC
5922
+ },
5923
+ parameters: {
5924
+ type: "object",
5925
+ properties: {
5926
+ receipt: {
5927
+ type: "object",
5928
+ description: "The PipRailReceipt JSON ({ piprail, receipt, resource, decimals? }) to re-verify."
5929
+ },
5930
+ rpcUrl: {
5931
+ type: "string",
5932
+ description: "Optional RPC URL for the receipt's chain (required for chains outside the common presets)."
5933
+ }
5934
+ },
5935
+ required: ["receipt"],
5936
+ additionalProperties: false
5937
+ },
5938
+ invoke: async (args) => {
5939
+ const receipt = args.receipt;
5940
+ const opts = args.rpcUrl ? { rpcUrl: String(args.rpcUrl) } : void 0;
5941
+ return await PipRailClient.verifyReceipt(receipt, opts);
5942
+ }
4622
5943
  }
4623
5944
  ];
4624
5945
  }
@@ -5268,17 +6589,40 @@ function normaliseExactOption(exact) {
5268
6589
  return exact;
5269
6590
  }
5270
6591
  var TRANSIENT_VERIFY_CODES = /* @__PURE__ */ new Set(["tx_not_found", "insufficient_confirmations"]);
6592
+ function nowUnixSeconds(verifiedAt) {
6593
+ if (verifiedAt) {
6594
+ const t = Date.parse(verifiedAt);
6595
+ if (!Number.isNaN(t)) return Math.floor(t / 1e3);
6596
+ }
6597
+ return Math.floor(Date.now() / 1e3);
6598
+ }
5271
6599
  function createPaymentGate(options) {
5272
6600
  const minConfirmations = options.minConfirmations ?? 1;
5273
6601
  const maxTimeoutSeconds = options.maxTimeoutSeconds ?? 600;
5274
6602
  const genNonce = options.generateNonce ?? (() => globalThis.crypto.randomUUID());
5275
6603
  const exactOption = normaliseExactOption(options.exact);
6604
+ const receiptsOn = options.receipts !== void 0 && options.receipts !== false;
6605
+ const receiptOpt = typeof options.receipts === "object" ? options.receipts : {};
6606
+ const receiptIncludeTxHash = receiptOpt.includeTxHash !== false;
6607
+ const receiptResourceUrl = receiptOpt.resource ?? "";
6608
+ const attestWallet = receiptOpt.attest && "wallet" in receiptOpt.attest ? receiptOpt.attest.wallet : void 0;
6609
+ const attestJws = receiptOpt.attest && "jws" in receiptOpt.attest;
6610
+ let attestWarned = false;
6611
+ function warnAttestDegrade(reason) {
6612
+ if (attestWarned) return;
6613
+ attestWarned = true;
6614
+ try {
6615
+ console.warn(`[piprail] receipts.attest ${reason}; emitting an unsigned Tier-1 receipt instead.`);
6616
+ } catch {
6617
+ }
6618
+ }
5276
6619
  let resolved;
5277
6620
  function ready() {
5278
6621
  if (resolved) return resolved;
5279
6622
  const p = (async () => {
5280
6623
  const accepts = normaliseAccepts(options);
5281
6624
  const exactSkips = [];
6625
+ const uptoSkips = [];
5282
6626
  const specs = await Promise.all(
5283
6627
  accepts.map(async (a) => {
5284
6628
  const net = await resolveNetwork2({ chain: a.chain, rpcUrl: a.rpcUrl ?? options.rpcUrl });
@@ -5297,9 +6641,19 @@ function createPaymentGate(options) {
5297
6641
  if (outcome.rail) spec.exact = outcome.rail;
5298
6642
  else if (outcome.skipReason) exactSkips.push(outcome.skipReason);
5299
6643
  }
6644
+ if (options.upto) {
6645
+ const upto = await resolveUptoRail(net, asset);
6646
+ if (upto) spec.upto = upto;
6647
+ else uptoSkips.push(`${net.network}/${asset}`);
6648
+ }
5300
6649
  return spec;
5301
6650
  })
5302
6651
  );
6652
+ if (options.upto && !specs.some((s) => s.upto)) {
6653
+ throw new Error(
6654
+ `requirePayment: \`upto\` (the metered rail) was requested but none of the offered rails support it (tried: ${uptoSkips.join(", ") || "none"}). The \`upto\` scheme is EVM-Permit2 ONLY \u2014 an ERC-20 on a chain with the x402UptoPermit2Proxy deployed (Ethereum/Base/Arbitrum/Optimism/Polygon/BNB), NOT a native coin, NOT a non-EVM family. (Native coins + non-Permit2 chains can never carry \`upto\`.)`
6655
+ );
6656
+ }
5303
6657
  if (exactOption && !specs.some((s) => s.exact)) {
5304
6658
  const why = exactSkips.length > 0 ? exactSkips.join(" ") : "The standard `exact` rail is EVM ERC-20 (EIP-3009 \u2014 USDC / EURC \u2014 or Permit2, e.g. Binance-Peg USDC on BNB) or a Solana SPL token (SVM) \u2014 NOT native coins, NOT families without a standard `exact` scheme.";
5305
6659
  if (exactOption.settle === "keyless") {
@@ -5379,7 +6733,27 @@ function createPaymentGate(options) {
5379
6733
  };
5380
6734
  return { rail: { method: info.method, ...info.extra ? { extra: info.extra } : {}, mode } };
5381
6735
  }
5382
- const hasCustomStore = Boolean(options.isUsed || options.markUsed);
6736
+ async function resolveUptoRail(net, asset) {
6737
+ const cfg = options.upto;
6738
+ if (!net.resolveUptoRail) return null;
6739
+ if (cfg.relayer === void 0) {
6740
+ throw new Error(
6741
+ "requirePayment: `upto` needs a `relayer` wallet (the gas-paying key that self-settles the metered actual AND is the bound witness.facilitator), e.g. upto: { relayer: { key }, settleAmount }."
6742
+ );
6743
+ }
6744
+ const relayer = net.bindWallet(cfg.relayer);
6745
+ const info = await net.resolveUptoRail({ asset, relayer });
6746
+ if (!info) return null;
6747
+ return { method: info.method, ...info.extra ? { extra: info.extra } : {}, relayer };
6748
+ }
6749
+ const hasIsUsed = typeof options.isUsed === "function";
6750
+ const hasMarkUsed = typeof options.markUsed === "function";
6751
+ if (hasIsUsed !== hasMarkUsed) {
6752
+ throw new Error(
6753
+ "requirePayment/createPaymentGate: `isUsed` and `markUsed` must be provided TOGETHER \u2014 a custom replay store needs both a read and a write. Supplying only " + (hasIsUsed ? "`isUsed`" : "`markUsed`") + " silently disables replay protection (double-spend). Provide both, or neither (the built-in in-memory store)."
6754
+ );
6755
+ }
6756
+ const hasCustomStore = hasIsUsed && hasMarkUsed;
5383
6757
  const localUsed = /* @__PURE__ */ new Map();
5384
6758
  const replayWindowMs = maxTimeoutSeconds * 1e3;
5385
6759
  function pruneUsed(now) {
@@ -5442,10 +6816,33 @@ function createPaymentGate(options) {
5442
6816
  }
5443
6817
  };
5444
6818
  }
6819
+ function buildUptoAccept(s) {
6820
+ const rail = s.upto;
6821
+ return {
6822
+ scheme: "upto",
6823
+ network: s.net.network,
6824
+ amount: s.amountBase.toString(),
6825
+ // the authorized MAX (base units)
6826
+ asset: s.asset,
6827
+ payTo: s.payTo,
6828
+ maxTimeoutSeconds,
6829
+ extra: {
6830
+ assetTransferMethod: "permit2-upto",
6831
+ // facilitatorAddress comes from rail.extra; the spread below carries it (+ name/version).
6832
+ facilitatorAddress: rail.extra?.facilitatorAddress ?? "",
6833
+ minConfirmations,
6834
+ decimals: s.decimals,
6835
+ amountFormatted: s.amountFormatted,
6836
+ ...s.symbol ? { symbol: s.symbol } : {},
6837
+ ...rail.extra
6838
+ }
6839
+ };
6840
+ }
5445
6841
  function buildAccepts(specs, nonce) {
5446
6842
  const out = [];
5447
6843
  for (const s of specs) {
5448
6844
  if (s.exact) out.push(buildExactAccept(s));
6845
+ if (s.upto) out.push(buildUptoAccept(s));
5449
6846
  out.push(buildAccept(s, nonce));
5450
6847
  }
5451
6848
  return out;
@@ -5463,7 +6860,8 @@ function createPaymentGate(options) {
5463
6860
  const selfDescribe = options.selfDescribe === false ? void 0 : buildSelfDescription({
5464
6861
  accepts,
5465
6862
  instruction: describeChallenge({ x402Version: 2, resource: { url: resourceUrl }, accepts }),
5466
- ...endpointInfo ? { endpoint: endpointInfo } : {}
6863
+ ...endpointInfo ? { endpoint: endpointInfo } : {},
6864
+ ...receiptsOn ? { verifiableReceipts: true } : {}
5467
6865
  });
5468
6866
  const rejectionExt = opts?.extensions ?? {};
5469
6867
  const rejectionPiprail = rejectionExt.piprail ?? {};
@@ -5552,6 +6950,55 @@ function createPaymentGate(options) {
5552
6950
  if (options.awaitOnPaid) await fireOnPaid(paid);
5553
6951
  else void fireOnPaid(paid);
5554
6952
  }
6953
+ async function buildPaidResult(spec, receipt, nonce) {
6954
+ if (!receiptsOn) {
6955
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
6956
+ }
6957
+ try {
6958
+ const stamped = {
6959
+ ...receipt,
6960
+ ...nonce ? { nonce } : {},
6961
+ // §5.3: a suppressed tx is the empty string on the wire, never a missing key.
6962
+ ...receiptIncludeTxHash ? {} : { transaction: "" }
6963
+ };
6964
+ const attestation = await maybeSignAttestation(spec, stamped);
6965
+ const extensions = buildReceiptExtension({
6966
+ receipt: stamped,
6967
+ resource: { url: receiptResourceUrl },
6968
+ decimals: spec.decimals,
6969
+ ...attestation ? { attestation } : {}
6970
+ });
6971
+ return { kind: "paid", receipt: stamped, receiptHeader: buildReceiptHeader(stamped, extensions) };
6972
+ } catch {
6973
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
6974
+ }
6975
+ }
6976
+ async function maybeSignAttestation(spec, stamped) {
6977
+ if (attestJws) {
6978
+ warnAttestDegrade("JWS attestation is not yet implemented (R3)");
6979
+ return void 0;
6980
+ }
6981
+ if (attestWallet === void 0) return void 0;
6982
+ if (typeof spec.net.signReceipt !== "function") {
6983
+ warnAttestDegrade(`is EVM-only; the ${spec.net.family} rail can't sign an EIP-712 attestation`);
6984
+ return void 0;
6985
+ }
6986
+ try {
6987
+ const wallet = spec.net.bindWallet(attestWallet);
6988
+ return await spec.net.signReceipt(wallet, {
6989
+ payTo: spec.payTo,
6990
+ network: spec.net.network,
6991
+ resourceUrl: receiptResourceUrl,
6992
+ payer: stamped.payer,
6993
+ issuedAt: nowUnixSeconds(stamped.verifiedAt),
6994
+ // §5.3: the signed message carries the empty string for a suppressed tx, never omitted.
6995
+ transaction: receiptIncludeTxHash ? stamped.transaction : ""
6996
+ });
6997
+ } catch {
6998
+ warnAttestDegrade("signing failed");
6999
+ return void 0;
7000
+ }
7001
+ }
5555
7002
  function reportOnFailedError(error, failure) {
5556
7003
  if (!options.onFailedError) return;
5557
7004
  try {
@@ -5578,6 +7025,14 @@ function createPaymentGate(options) {
5578
7025
  if (options.awaitOnFailed) await fireOnFailed(failure);
5579
7026
  else void fireOnFailed(failure);
5580
7027
  }
7028
+ function railExtra(full) {
7029
+ const out = {};
7030
+ for (const k of ["assetTransferMethod", "facilitatorAddress", "name", "version"]) {
7031
+ const v = full[k];
7032
+ if (v !== void 0 && v !== "") out[k] = v;
7033
+ }
7034
+ return Object.keys(out).length > 0 ? { extra: out } : {};
7035
+ }
5581
7036
  async function describe(resourceUrl = "") {
5582
7037
  const specs = await ready();
5583
7038
  const accepts = [];
@@ -5592,7 +7047,8 @@ function createPaymentGate(options) {
5592
7047
  maxTimeoutSeconds,
5593
7048
  ...s.symbol ? { symbol: s.symbol } : {}
5594
7049
  };
5595
- if (s.exact) accepts.push({ scheme: "exact", ...base2 });
7050
+ if (s.exact) accepts.push({ scheme: "exact", ...base2, ...railExtra(buildExactAccept(s).extra) });
7051
+ if (s.upto) accepts.push({ scheme: "upto", ...base2, ...railExtra(buildUptoAccept(s).extra) });
5596
7052
  accepts.push({ scheme: "onchain-proof", ...base2 });
5597
7053
  }
5598
7054
  return {
@@ -5628,7 +7084,7 @@ function createPaymentGate(options) {
5628
7084
  }
5629
7085
  await settleTx(ref, true);
5630
7086
  await deliverOnPaid(spec, result.receipt);
5631
- return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
7087
+ return await buildPaidResult(spec, result.receipt, sig.payload.nonce);
5632
7088
  }
5633
7089
  async function verifyExact(exact) {
5634
7090
  const specs = await ready();
@@ -5738,27 +7194,122 @@ function createPaymentGate(options) {
5738
7194
  }
5739
7195
  await settleTx(nonce, true);
5740
7196
  await deliverOnPaid(spec, result.receipt);
5741
- return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
7197
+ return await buildPaidResult(spec, result.receipt);
7198
+ }
7199
+ function resolveSettleAmount(raw, maxAmount, decimals) {
7200
+ if (typeof raw === "bigint") return raw < 0n ? null : raw;
7201
+ const s = raw.trim();
7202
+ if (s.length === 0) return null;
7203
+ try {
7204
+ if (s.endsWith("%")) {
7205
+ const pct = s.slice(0, -1).trim();
7206
+ if (!/^\d+(\.\d+)?$/.test(pct)) return null;
7207
+ const pctScaled = floorUnits(pct, 4);
7208
+ return maxAmount * pctScaled / (100n * 10n ** 4n);
7209
+ }
7210
+ if (s.startsWith("$")) {
7211
+ const amt = s.slice(1).trim();
7212
+ return floorUnits(amt, decimals);
7213
+ }
7214
+ if (/^\d+$/.test(s)) return BigInt(s);
7215
+ return floorUnits(s, decimals);
7216
+ } catch {
7217
+ return null;
7218
+ }
7219
+ }
7220
+ async function verifyUpto(upto) {
7221
+ const specs = await ready();
7222
+ const uptoSpecs = specs.filter((s) => s.upto);
7223
+ if (uptoSpecs.length === 0) {
7224
+ return rejection("transfer_not_found", "This resource offers no standard `upto` (metered) rail.");
7225
+ }
7226
+ const isCaip = upto.network.includes(":");
7227
+ let candidates = isCaip ? uptoSpecs.filter((s) => s.net.network === upto.network) : uptoSpecs;
7228
+ if (upto.asset) {
7229
+ candidates = candidates.filter((s) => s.asset.toLowerCase() === upto.asset.toLowerCase());
7230
+ }
7231
+ let spec = candidates[0];
7232
+ if (!isCaip && !upto.asset && uptoSpecs.length > 1) spec = void 0;
7233
+ if (!spec && !isCaip && !upto.asset && uptoSpecs.length === 1) spec = uptoSpecs[0];
7234
+ if (!spec || !spec.upto) {
7235
+ return rejection(
7236
+ "transfer_not_found",
7237
+ `No \`upto\` rail offered for ${upto.network}${upto.asset ? `/${upto.asset}` : ""} (offered: ${uptoSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7238
+ );
7239
+ }
7240
+ const nonce = upto.payload.permit2Authorization.nonce;
7241
+ if (await claimTx(nonce)) {
7242
+ return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
7243
+ }
7244
+ const accept = buildUptoAccept(spec);
7245
+ const relayer = spec.upto.relayer;
7246
+ let result;
7247
+ try {
7248
+ const rawAmount = await options.upto.settleAmount({
7249
+ maxAmount: spec.amountBase,
7250
+ asset: spec.asset,
7251
+ network: spec.net.network,
7252
+ decimals: spec.decimals,
7253
+ ...upto.raw ? { request: upto.raw } : {}
7254
+ });
7255
+ const settleAmount = resolveSettleAmount(rawAmount, spec.amountBase, spec.decimals);
7256
+ if (settleAmount === null) {
7257
+ await settleTx(nonce, false);
7258
+ return rejection(
7259
+ "upto_settle_exceeds_max",
7260
+ `The settleAmount callback returned an unparseable amount (${String(rawAmount)}). Return a bigint, a raw/"NN%"/"$X" string, or 0.`
7261
+ );
7262
+ }
7263
+ result = await spec.net.settleUptoSelf({ relayer, payload: upto.payload, accept, settleAmount });
7264
+ } catch (err) {
7265
+ await settleTx(nonce, false);
7266
+ if (err instanceof SettlementError) throw err;
7267
+ return rejection(
7268
+ "tx_reverted",
7269
+ `upto: metering/settle failed (${err instanceof Error ? err.message : String(err)}).`
7270
+ );
7271
+ }
7272
+ if (!result.ok) {
7273
+ await settleTx(nonce, false);
7274
+ return rejection(result.error, result.detail);
7275
+ }
7276
+ await settleTx(nonce, true);
7277
+ await deliverOnPaid(spec, result.receipt);
7278
+ return await buildPaidResult(spec, result.receipt);
5742
7279
  }
5743
7280
  async function verify(paymentSignature) {
5744
- const result = await resolveVerdict(paymentSignature);
7281
+ const raw = normaliseHeader(paymentSignature);
7282
+ const result = await resolveVerdictObject(raw === void 0 ? void 0 : decodeBase64Json(raw));
5745
7283
  if (result.kind === "invalid") await deliverOnFailed(result);
5746
7284
  return result;
5747
7285
  }
5748
- async function resolveVerdict(paymentSignature) {
5749
- const raw = normaliseHeader(paymentSignature);
5750
- if (!raw) return asChallenge();
5751
- const sig = parseSignatureHeader(raw);
7286
+ async function verifyObject(payload) {
7287
+ const result = await resolveVerdictObject(payload);
7288
+ if (result.kind === "invalid") await deliverOnFailed(result);
7289
+ return result;
7290
+ }
7291
+ async function resolveVerdictObject(obj) {
7292
+ if (obj === void 0 || obj === null) return asChallenge();
7293
+ const sig = parseSignatureObject(obj);
5752
7294
  if (sig && sig.accepted && typeof sig.accepted.network === "string" && typeof sig.accepted.asset === "string") {
5753
7295
  return verifyOnchainProof(sig);
5754
7296
  }
5755
- const exact = parseExactPaymentHeader(raw);
7297
+ const upto = parseUptoObject(obj);
7298
+ if (upto) return verifyUpto(upto);
7299
+ const exact = parseExactObject(obj);
5756
7300
  if (exact) return verifyExact(exact);
5757
7301
  return asChallenge();
5758
7302
  }
5759
- return { challenge, verify, describe, landingPage };
7303
+ return { challenge, verify, verifyObject, describe, landingPage };
5760
7304
  }
5761
7305
  function requirePayment(options) {
7306
+ if (options.upto) {
7307
+ throw new class extends PipRailError {
7308
+ code = "UNSUPPORTED_SCHEME";
7309
+ }(
7310
+ "requirePayment: the 'upto' (metered) rail is unsupported through the Express middleware \u2014 it settles before the route handler serves, so metered usage is unknown at settle time. Call gate.verify() directly (createPaymentGate) and meter inside settleAmount; see docs/accepting-payments/upto-rail-seller.md."
7311
+ );
7312
+ }
5762
7313
  const gate = createPaymentGate(options);
5763
7314
  return async (req, res, next) => {
5764
7315
  let result;
@@ -5904,13 +7455,261 @@ async function deliverReceipt(receipt, options) {
5904
7455
  ...lastError ? { error: lastError } : {}
5905
7456
  };
5906
7457
  }
7458
+
7459
+ // src/transports/a2a.ts
7460
+ var A2A_X402_EXTENSION_URI_V01 = "https://github.com/google-a2a/a2a-x402/v0.1";
7461
+ var A2A_X402_EXTENSION_URI_V02 = "https://github.com/google-agentic-commerce/a2a-x402/blob/main/spec/v0.2";
7462
+ var A2A_STATUS_KEY = "x402.payment.status";
7463
+ var A2A_REQUIRED_KEY = "x402.payment.required";
7464
+ var A2A_PAYLOAD_KEY = "x402.payment.payload";
7465
+ var A2A_RECEIPTS_KEY = "x402.payment.receipts";
7466
+ var A2A_ERROR_KEY = "x402.payment.error";
7467
+ var A2A_EXTENSIONS_HEADER = "X-A2A-Extensions";
7468
+ var MAX_TASK_RECEIPTS = 64;
7469
+ var VERIFY_CODE_TO_A2A_ERROR = {
7470
+ payment_expired: "EXPIRED_PAYMENT",
7471
+ tx_already_used: "DUPLICATE_NONCE",
7472
+ amount_too_low: "INVALID_AMOUNT",
7473
+ upto_settle_exceeds_max: "INVALID_AMOUNT",
7474
+ signature_invalid: "INVALID_SIGNATURE",
7475
+ transfer_not_found: "INVALID_AMOUNT",
7476
+ wrong_recipient: "INVALID_AMOUNT",
7477
+ tx_reverted: "SETTLEMENT_FAILED",
7478
+ tx_not_found: "EXPIRED_PAYMENT",
7479
+ insufficient_confirmations: "EXPIRED_PAYMENT",
7480
+ no_meta: "INVALID_SIGNATURE",
7481
+ // The settlement-side throw code the handler emits for a SettlementError (the merchant's
7482
+ // relayer/facilitator never moved funds) — maps to the spec's SETTLEMENT_FAILED.
7483
+ settlement_failed: "SETTLEMENT_FAILED"
7484
+ };
7485
+ function toA2AErrorCode(code) {
7486
+ return VERIFY_CODE_TO_A2A_ERROR[code] ?? code;
7487
+ }
7488
+ function toA2APaymentRequired(taskId, challenge, parts) {
7489
+ const metadata = {
7490
+ [A2A_STATUS_KEY]: "payment-required",
7491
+ [A2A_REQUIRED_KEY]: challenge
7492
+ };
7493
+ return {
7494
+ kind: "task",
7495
+ id: taskId,
7496
+ status: {
7497
+ state: "input-required",
7498
+ message: { kind: "message", role: "agent", taskId, ...parts ? { parts } : {}, metadata }
7499
+ }
7500
+ };
7501
+ }
7502
+ function toA2APaymentReceipts(receipts) {
7503
+ return { [A2A_RECEIPTS_KEY]: receipts };
7504
+ }
7505
+ function toA2APaymentFailed(code, detail, receipts = [], network) {
7506
+ const entry = {
7507
+ success: false,
7508
+ transaction: "",
7509
+ ...network ? { network } : {},
7510
+ errorReason: `${code}: ${detail}`
7511
+ };
7512
+ return {
7513
+ [A2A_STATUS_KEY]: "payment-failed",
7514
+ [A2A_ERROR_KEY]: toA2AErrorCode(code),
7515
+ [A2A_RECEIPTS_KEY]: [...receipts, entry]
7516
+ };
7517
+ }
7518
+ function fromA2APaymentRequired(task) {
7519
+ const meta = task.status?.message?.metadata;
7520
+ const required = meta?.[A2A_REQUIRED_KEY];
7521
+ if (!required || typeof required !== "object") return null;
7522
+ return required;
7523
+ }
7524
+ function fromA2APaymentPayload(message) {
7525
+ const raw = message.metadata?.[A2A_PAYLOAD_KEY];
7526
+ if (raw === void 0 || raw === null) return null;
7527
+ return { raw, taskId: message.taskId ?? "" };
7528
+ }
7529
+ function defaultTaskStore() {
7530
+ const map = /* @__PURE__ */ new Map();
7531
+ function prune(now) {
7532
+ for (const [key, { expiry }] of map) {
7533
+ if (expiry > now) break;
7534
+ map.delete(key);
7535
+ }
7536
+ }
7537
+ return {
7538
+ get(taskId) {
7539
+ const now = Date.now();
7540
+ prune(now);
7541
+ const hit = map.get(taskId);
7542
+ return hit && hit.expiry > now ? hit.record : void 0;
7543
+ },
7544
+ set(taskId, value, ttlMs) {
7545
+ const now = Date.now();
7546
+ prune(now);
7547
+ map.delete(taskId);
7548
+ map.set(taskId, { record: value, expiry: now + ttlMs });
7549
+ }
7550
+ };
7551
+ }
7552
+ function createA2APaymentHandler(options) {
7553
+ const gate = options.gate ?? createPaymentGate(options);
7554
+ const maxTimeoutSeconds = options.maxTimeoutSeconds ?? 600;
7555
+ const ttlMs = options.taskTtlMs ?? maxTimeoutSeconds * 1e3;
7556
+ const store = options.taskStore ?? defaultTaskStore();
7557
+ function appendReceipt(taskId, entry) {
7558
+ const prior = store.get(taskId)?.receipts ?? [];
7559
+ const isDup = "success" in entry && entry.success === true && prior.some((r) => "transaction" in r && r.transaction === entry.transaction);
7560
+ const next = isDup ? prior : [...prior, entry];
7561
+ const receipts = next.length > MAX_TASK_RECEIPTS ? next.slice(-MAX_TASK_RECEIPTS) : next;
7562
+ store.set(taskId, { receipts }, ttlMs);
7563
+ return receipts;
7564
+ }
7565
+ function completedTask(taskId, receipts, artifacts, status = "payment-completed") {
7566
+ const metadata = { [A2A_STATUS_KEY]: status, ...toA2APaymentReceipts(receipts) };
7567
+ return {
7568
+ kind: "task",
7569
+ id: taskId,
7570
+ status: { state: "completed", message: { kind: "message", role: "agent", taskId, metadata } },
7571
+ ...artifacts.length > 0 ? { artifacts } : {}
7572
+ };
7573
+ }
7574
+ function reChallengeTask(taskId, result, attempted) {
7575
+ if (result.kind !== "invalid") {
7576
+ const metadata2 = {
7577
+ [A2A_STATUS_KEY]: "payment-required",
7578
+ [A2A_REQUIRED_KEY]: result.challenge
7579
+ };
7580
+ return {
7581
+ kind: "task",
7582
+ id: taskId,
7583
+ status: { state: "input-required", message: { kind: "message", role: "agent", taskId, metadata: metadata2 } }
7584
+ };
7585
+ }
7586
+ const network = attempted ?? singleNetworkOf(result.challenge);
7587
+ const receipts = appendReceiptFailed(taskId, result.error, result.detail, network);
7588
+ const metadata = {
7589
+ [A2A_STATUS_KEY]: "payment-rejected",
7590
+ [A2A_ERROR_KEY]: toA2AErrorCode(result.error),
7591
+ [A2A_REQUIRED_KEY]: result.challenge,
7592
+ ...toA2APaymentReceipts(receipts)
7593
+ };
7594
+ return {
7595
+ kind: "task",
7596
+ id: taskId,
7597
+ status: { state: "input-required", message: { kind: "message", role: "agent", taskId, metadata } }
7598
+ };
7599
+ }
7600
+ function failedTask(taskId, code, detail, network) {
7601
+ const receipts = appendReceiptFailed(taskId, code, detail, network);
7602
+ const metadata = {
7603
+ [A2A_STATUS_KEY]: "payment-failed",
7604
+ [A2A_ERROR_KEY]: toA2AErrorCode(code),
7605
+ ...toA2APaymentReceipts(receipts)
7606
+ };
7607
+ return {
7608
+ kind: "task",
7609
+ id: taskId,
7610
+ status: { state: "failed", message: { kind: "message", role: "agent", taskId, metadata } }
7611
+ };
7612
+ }
7613
+ function appendReceiptFailed(taskId, code, detail, network) {
7614
+ return appendReceipt(taskId, {
7615
+ success: false,
7616
+ transaction: "",
7617
+ ...network ? { network } : {},
7618
+ errorReason: `${code}: ${detail}`
7619
+ });
7620
+ }
7621
+ async function handleMessage(message, taskId) {
7622
+ const id = taskId ?? message.taskId ?? newTaskId();
7623
+ const inbound = fromA2APaymentPayload(message);
7624
+ if (!inbound) {
7625
+ const { challenge } = await gate.challenge(resourceUrlFromMessage(message));
7626
+ store.set(id, { receipts: [] }, ttlMs);
7627
+ return toA2APaymentRequired(id, challenge);
7628
+ }
7629
+ const attempted = networkFromPayload(inbound.raw);
7630
+ let result;
7631
+ try {
7632
+ result = await gate.verifyObject(inbound.raw);
7633
+ } catch (err) {
7634
+ if (err instanceof SettlementError) {
7635
+ return failedTask(id, "settlement_failed", err.message, attempted);
7636
+ }
7637
+ const detail = err instanceof Error ? err.message : String(err);
7638
+ return failedTask(id, "tx_reverted", detail, attempted);
7639
+ }
7640
+ switch (result.kind) {
7641
+ case "challenge":
7642
+ return reChallengeTask(id, result, attempted);
7643
+ case "invalid":
7644
+ return reChallengeTask(id, result, attempted);
7645
+ case "paid": {
7646
+ const receipts = appendReceipt(id, result.receipt);
7647
+ let artifacts = [];
7648
+ if (options.fulfill) {
7649
+ try {
7650
+ artifacts = await options.fulfill({ taskId: id, receipt: result.receipt, message }) ?? [];
7651
+ } catch (err) {
7652
+ const detail = err instanceof Error ? err.message : String(err);
7653
+ const annotation = {
7654
+ name: "fulfillment-error",
7655
+ metadata: { "x402.fulfillment.error": detail, "x402.fulfillment.settled": true },
7656
+ parts: [{ kind: "text", text: `Payment settled, but serving the result failed: ${detail}` }]
7657
+ };
7658
+ return completedTask(id, receipts, [annotation]);
7659
+ }
7660
+ }
7661
+ return completedTask(id, receipts, artifacts);
7662
+ }
7663
+ }
7664
+ }
7665
+ function agentCardExtension(opts) {
7666
+ const uri = opts?.version === "v0.2" ? A2A_X402_EXTENSION_URI_V02 : A2A_X402_EXTENSION_URI_V01;
7667
+ return {
7668
+ uri,
7669
+ description: "Supports payments using the x402 protocol for on-chain settlement.",
7670
+ ...opts?.required ? { required: true } : {}
7671
+ };
7672
+ }
7673
+ return { handleMessage, agentCardExtension, gate };
7674
+ }
7675
+ function singleNetworkOf(challenge) {
7676
+ const nets = new Set((challenge.accepts ?? []).map((a) => a.network).filter(Boolean));
7677
+ return nets.size === 1 ? [...nets][0] : void 0;
7678
+ }
7679
+ function networkFromPayload(raw) {
7680
+ const v = raw;
7681
+ if (v && typeof v.accepted?.network === "string") return v.accepted.network;
7682
+ if (v && typeof v.network === "string") return v.network;
7683
+ return void 0;
7684
+ }
7685
+ function newTaskId() {
7686
+ return `task-${globalThis.crypto.randomUUID()}`;
7687
+ }
7688
+ function resourceUrlFromMessage(message) {
7689
+ for (const part of message.parts ?? []) {
7690
+ const data = part.data;
7691
+ if (data && typeof data.url === "string") return data.url;
7692
+ }
7693
+ return "";
7694
+ }
5907
7695
  export {
7696
+ A2A_ERROR_KEY,
7697
+ A2A_EXTENSIONS_HEADER,
7698
+ A2A_PAYLOAD_KEY,
7699
+ A2A_RECEIPTS_KEY,
7700
+ A2A_REQUIRED_KEY,
7701
+ A2A_STATUS_KEY,
7702
+ A2A_X402_EXTENSION_URI_V01,
7703
+ A2A_X402_EXTENSION_URI_V02,
5908
7704
  BRAND,
7705
+ BUILTIN_DENOMS,
5909
7706
  CHAINS,
5910
7707
  ConfirmationTimeoutError,
7708
+ DENOM_PRECISION,
5911
7709
  DIRECTORY_INFO,
5912
7710
  EIP3009_TYPES,
5913
7711
  EXACT_NETWORK_SLUGS,
7712
+ EXT_OFFER_RECEIPT,
5914
7713
  GENERATOR,
5915
7714
  HEADER_REQUIRED,
5916
7715
  HEADER_RESPONSE,
@@ -5927,6 +7726,7 @@ export {
5927
7726
  NonReplayableBodyError,
5928
7727
  PERMIT2_ADDRESS,
5929
7728
  PERMIT2_PROXY_CHAIN_IDS,
7729
+ PERMIT2_UPTO_WITNESS_TYPES,
5930
7730
  PERMIT2_WITNESS_TYPES,
5931
7731
  PIPRAIL_AGENT_GUIDE,
5932
7732
  POWERED_BY,
@@ -5937,13 +7737,17 @@ export {
5937
7737
  REGISTER_ATTRIBUTION,
5938
7738
  RecipientNotReadyError,
5939
7739
  SettlementError,
7740
+ SpendLedger,
7741
+ UPTO_PROXY_CHAIN_IDS,
5940
7742
  UnknownTokenError,
5941
7743
  UnsupportedNetworkError,
5942
7744
  UnsupportedSchemeError,
7745
+ VERIFY_CODE_TO_A2A_ERROR,
5943
7746
  WalletRequiredError,
5944
7747
  WrongChainError,
5945
7748
  WrongFamilyError,
5946
7749
  X402_EXACT_PERMIT2_PROXY,
7750
+ X402_UPTO_PERMIT2_PROXY,
5947
7751
  agentGuide,
5948
7752
  appendAttribution,
5949
7753
  appendKeywords,
@@ -5953,17 +7757,22 @@ export {
5953
7757
  buildExactAuthorization,
5954
7758
  buildExactSignatureHeader,
5955
7759
  buildOpenApi,
7760
+ buildReceiptExtension,
5956
7761
  buildReceiptHeader,
5957
7762
  buildSelfDescription,
5958
7763
  buildSignatureHeader,
7764
+ buildUptoSignatureHeader,
5959
7765
  buildWellKnownX402,
5960
7766
  buildX402DnsTxt,
5961
7767
  chainIdForExactNetwork,
5962
7768
  claim402IndexDomain,
5963
7769
  classifyChallenge,
7770
+ createA2APaymentHandler,
5964
7771
  createPaymentGate,
7772
+ decodeBase64Json,
5965
7773
  decorateOutcome,
5966
7774
  deliverReceipt,
7775
+ denomOf,
5967
7776
  describeChallenge,
5968
7777
  discoveryHeaders,
5969
7778
  eip3009Abi,
@@ -5974,17 +7783,26 @@ export {
5974
7783
  fetchAcross,
5975
7784
  firstKeylessFacilitator,
5976
7785
  formatSpendReport,
7786
+ fromA2APaymentPayload,
7787
+ fromA2APaymentRequired,
5977
7788
  getDirectoryInfo,
5978
7789
  isPermit2ProxyChain,
7790
+ isUptoProxyChain,
5979
7791
  knownFacilitatorsFor,
7792
+ memorySpendStore,
5980
7793
  normalizeNetwork,
5981
7794
  parseChallenge,
7795
+ parseExactObject,
5982
7796
  parseExactPaymentHeader,
5983
7797
  parseExactRequirements,
5984
7798
  parseFacilitatorSupported,
5985
7799
  parseReceipt,
7800
+ parseReceiptExtension,
5986
7801
  parseSettleResponse,
5987
7802
  parseSignatureHeader,
7803
+ parseSignatureObject,
7804
+ parseUptoObject,
7805
+ parseUptoPaymentHeader,
5988
7806
  paymentTools,
5989
7807
  pickAccept,
5990
7808
  planAcross,
@@ -6000,6 +7818,10 @@ export {
6000
7818
  searchOpenIndexes,
6001
7819
  settleViaFacilitator,
6002
7820
  summarizePlan,
7821
+ toA2AErrorCode,
7822
+ toA2APaymentFailed,
7823
+ toA2APaymentReceipts,
7824
+ toA2APaymentRequired,
6003
7825
  toInsufficientFundsError,
6004
7826
  toInvalidBody,
6005
7827
  verify402IndexDomain