@aifinpay/agent 2.0.0-rc.8 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/PAYMENT_RECEIPTS.md +114 -0
  3. package/README.md +85 -28
  4. package/dist/agent.d.ts +8 -1
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +50 -17
  7. package/dist/agent.js.map +1 -1
  8. package/dist/agentHistory.d.ts +55 -0
  9. package/dist/agentHistory.d.ts.map +1 -0
  10. package/dist/agentHistory.js +145 -0
  11. package/dist/agentHistory.js.map +1 -0
  12. package/dist/agentPassport.d.ts +71 -7
  13. package/dist/agentPassport.d.ts.map +1 -1
  14. package/dist/agentPassport.js +135 -27
  15. package/dist/agentPassport.js.map +1 -1
  16. package/dist/aifp1.d.ts +94 -5
  17. package/dist/aifp1.d.ts.map +1 -1
  18. package/dist/aifp1.js +370 -114
  19. package/dist/aifp1.js.map +1 -1
  20. package/dist/apiUrl.d.ts +3 -0
  21. package/dist/apiUrl.d.ts.map +1 -0
  22. package/dist/apiUrl.js +8 -0
  23. package/dist/apiUrl.js.map +1 -0
  24. package/dist/chains.d.ts +7 -0
  25. package/dist/chains.d.ts.map +1 -1
  26. package/dist/chains.js +19 -1
  27. package/dist/chains.js.map +1 -1
  28. package/dist/crossChain.d.ts.map +1 -1
  29. package/dist/crossChain.js.map +1 -1
  30. package/dist/crypto.d.ts.map +1 -1
  31. package/dist/crypto.js.map +1 -1
  32. package/dist/deploymentResolver.d.ts +95 -0
  33. package/dist/deploymentResolver.d.ts.map +1 -0
  34. package/dist/deploymentResolver.js +169 -0
  35. package/dist/deploymentResolver.js.map +1 -0
  36. package/dist/errors.d.ts +17 -0
  37. package/dist/errors.d.ts.map +1 -1
  38. package/dist/errors.js +14 -0
  39. package/dist/errors.js.map +1 -1
  40. package/dist/facilitators/aifinpay.d.ts +4 -5
  41. package/dist/facilitators/aifinpay.d.ts.map +1 -1
  42. package/dist/facilitators/aifinpay.js +50 -21
  43. package/dist/facilitators/aifinpay.js.map +1 -1
  44. package/dist/facilitators/base.d.ts +12 -1
  45. package/dist/facilitators/base.d.ts.map +1 -1
  46. package/dist/facilitators/coinbase.d.ts.map +1 -1
  47. package/dist/facilitators/coinbase.js +3 -7
  48. package/dist/facilitators/coinbase.js.map +1 -1
  49. package/dist/facilitators/detect.d.ts.map +1 -1
  50. package/dist/facilitators/detect.js +2 -7
  51. package/dist/facilitators/detect.js.map +1 -1
  52. package/dist/facilitators/standard-x402.d.ts.map +1 -1
  53. package/dist/facilitators/standard-x402.js +40 -30
  54. package/dist/facilitators/standard-x402.js.map +1 -1
  55. package/dist/generated/solanaV14Deployments.generated.d.ts +25 -0
  56. package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -0
  57. package/dist/generated/solanaV14Deployments.generated.js +39 -0
  58. package/dist/generated/solanaV14Deployments.generated.js.map +1 -0
  59. package/dist/generated/splitterRoutes.generated.d.ts +24 -0
  60. package/dist/generated/splitterRoutes.generated.d.ts.map +1 -0
  61. package/dist/generated/splitterRoutes.generated.js +453 -0
  62. package/dist/generated/splitterRoutes.generated.js.map +1 -0
  63. package/dist/generated/v14Deployments.generated.d.ts +49 -0
  64. package/dist/generated/v14Deployments.generated.d.ts.map +1 -0
  65. package/dist/generated/v14Deployments.generated.js +415 -0
  66. package/dist/generated/v14Deployments.generated.js.map +1 -0
  67. package/dist/index.d.ts +27 -13
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +16 -7
  70. package/dist/index.js.map +1 -1
  71. package/dist/localIdentity.d.ts +17 -0
  72. package/dist/localIdentity.d.ts.map +1 -0
  73. package/dist/localIdentity.js +95 -0
  74. package/dist/localIdentity.js.map +1 -0
  75. package/dist/settlement.d.ts +34 -4
  76. package/dist/settlement.d.ts.map +1 -1
  77. package/dist/settlement.js +233 -75
  78. package/dist/settlement.js.map +1 -1
  79. package/dist/settlementHttp.d.ts +10 -0
  80. package/dist/settlementHttp.d.ts.map +1 -0
  81. package/dist/settlementHttp.js +78 -0
  82. package/dist/settlementHttp.js.map +1 -0
  83. package/dist/settlementV14.d.ts +10 -9
  84. package/dist/settlementV14.d.ts.map +1 -1
  85. package/dist/settlementV14.js +43 -55
  86. package/dist/settlementV14.js.map +1 -1
  87. package/dist/solanaDeploymentResolver.d.ts +98 -0
  88. package/dist/solanaDeploymentResolver.d.ts.map +1 -0
  89. package/dist/solanaDeploymentResolver.js +162 -0
  90. package/dist/solanaDeploymentResolver.js.map +1 -0
  91. package/dist/solanaV14Deployments.generated.d.ts +25 -0
  92. package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
  93. package/dist/solanaV14Deployments.generated.js +39 -0
  94. package/dist/solanaV14Deployments.generated.js.map +1 -0
  95. package/dist/spendLedger.d.ts.map +1 -1
  96. package/dist/spendLedger.js +1 -3
  97. package/dist/spendLedger.js.map +1 -1
  98. package/dist/splitterRoutes.d.ts +11 -4
  99. package/dist/splitterRoutes.d.ts.map +1 -1
  100. package/dist/splitterRoutes.generated.d.ts +11 -9
  101. package/dist/splitterRoutes.generated.d.ts.map +1 -1
  102. package/dist/splitterRoutes.generated.js +72 -18
  103. package/dist/splitterRoutes.generated.js.map +1 -1
  104. package/dist/splitterRoutes.js +7 -5
  105. package/dist/splitterRoutes.js.map +1 -1
  106. package/dist/unifiedAgent.d.ts +96 -28
  107. package/dist/unifiedAgent.d.ts.map +1 -1
  108. package/dist/unifiedAgent.js +219 -348
  109. package/dist/unifiedAgent.js.map +1 -1
  110. package/dist/v14Deployments.generated.d.ts +49 -0
  111. package/dist/v14Deployments.generated.d.ts.map +1 -0
  112. package/dist/v14Deployments.generated.js +413 -0
  113. package/dist/v14Deployments.generated.js.map +1 -0
  114. package/dist/wallet.d.ts.map +1 -1
  115. package/dist/wallet.js +2 -2
  116. package/dist/wallet.js.map +1 -1
  117. package/package.json +27 -6
  118. package/dist/cli.d.ts +0 -3
  119. package/dist/cli.d.ts.map +0 -1
  120. package/dist/cli.js +0 -127
  121. package/dist/cli.js.map +0 -1
@@ -14,12 +14,14 @@ import { blake2b } from "@noble/hashes/blake2b";
14
14
  import bs58 from "bs58";
15
15
  import { createHash, randomBytes } from "node:crypto";
16
16
  import { createPublicClient, createWalletClient, http, keccak256, toHex, toFunctionSelector, formatEther, } from "viem";
17
- import { privateKeyToAccount, } from "viem/accounts";
17
+ import { privateKeyToAccount } from "viem/accounts";
18
18
  import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain } from "viem/chains";
19
19
  import { botchain, xrplevm } from "./chains.js";
20
- import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction, } from "@solana/web3.js";
21
- import { AiFinPayError, X402Error, } from "./errors.js";
20
+ import { AiFinPayError, X402Error } from "./errors.js";
22
21
  import { Agent } from "./agent.js";
22
+ import { loadLocalWalletIdentity } from "./localIdentity.js";
23
+ import { SettlementClient, executeSettlementInvoice, } from "./settlement.js";
24
+ import { getQuota } from "./agentHistory.js";
23
25
  import { MemorySpendLedger, FileSpendLedger } from "./spendLedger.js";
24
26
  import { aifp1Fetch, Aifp1ReceiptCache, } from "./aifp1.js";
25
27
  import { bridgeQuote as crossChainQuote, bridgeExecute as crossChainExecute, bridgeWaitForArrival, USDC_NATIVE, } from "./crossChain.js";
@@ -132,6 +134,7 @@ export const SPLITTER_DEPLOYMENTS = {
132
134
  nativeUsdEnv: "AIFINPAY_ETH_USD",
133
135
  nativeUsdDefault: 1870, // reference only; ~$1870 on 2026-08-03
134
136
  },
137
+ /** @deprecated BOT Chain (chainId: 677) is deprecated. Use robinhood (chainId: 4663) instead. */
135
138
  botchain: {
136
139
  version: "1.2",
137
140
  chainId: 677,
@@ -165,8 +168,8 @@ const EVM_CHAIN_OBJECTS = {
165
168
  base,
166
169
  };
167
170
  function evmChainObject(name) {
168
- return (EVM_CHAIN_OBJECTS[name]
169
- ?? SPLITTER_DEPLOYMENTS[name]?.chain);
171
+ return (EVM_CHAIN_OBJECTS[name] ??
172
+ SPLITTER_DEPLOYMENTS[name]?.chain);
170
173
  }
171
174
  // Mainnet USDC SPL mint on Solana (Circle native, not Wormhole-wrapped USDCet)
172
175
  const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
@@ -295,8 +298,7 @@ export class AiFinPayAgent {
295
298
  constructor(inner, evmAccount, opts = {}) {
296
299
  this.inner = inner;
297
300
  this.evmAccount = evmAccount;
298
- this.registryUrl = opts.registryUrl
299
- ?? `${inner.baseUrl}${DEFAULT_REGISTRY_PATH}`;
301
+ this.registryUrl = opts.registryUrl ?? `${inner.baseUrl}${DEFAULT_REGISTRY_PATH}`;
300
302
  this.registryCandidates = opts.registryUrl
301
303
  ? [opts.registryUrl]
302
304
  : DEFAULT_REGISTRY_PATHS.map((path) => `${inner.baseUrl}${path}`);
@@ -311,9 +313,7 @@ export class AiFinPayAgent {
311
313
  this.ledgerOverride = opts.spendLedger;
312
314
  this.telemetry = opts.telemetry !== false;
313
315
  this.polygonRpc = opts.polygonRpc ?? "https://polygon.drpc.org";
314
- this.solanaRpc = opts.solanaRpc
315
- ?? process.env.AIFINPAY_SOLANA_RPC
316
- ?? "https://api.mainnet-beta.solana.com";
316
+ this.solanaRpc = opts.solanaRpc ?? process.env.AIFINPAY_SOLANA_RPC ?? "https://api.mainnet-beta.solana.com";
317
317
  // Optional RPC overrides for non-Polygon EVM chains used in bridge flows.
318
318
  // Falls back to viem's chain.rpcUrls.default if not provided.
319
319
  if (opts.evmRpcUrls)
@@ -354,8 +354,7 @@ export class AiFinPayAgent {
354
354
  if (!chain) {
355
355
  throw new AiFinPayError(`evmClients: unsupported EVM chain "${name}"`);
356
356
  }
357
- const rpcUrl = this.evmRpcUrls[name]
358
- ?? SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
357
+ const rpcUrl = this.evmRpcUrls[name] ?? SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
359
358
  const transport = rpcUrl ? http(rpcUrl) : http();
360
359
  const publicClient = createPublicClient({ chain, transport });
361
360
  const walletClient = createWalletClient({ chain, transport, account: this.evmAccount });
@@ -373,8 +372,30 @@ export class AiFinPayAgent {
373
372
  }
374
373
  // ── Constructors ─────────────────────────────────────────────────────────
375
374
  /**
376
- * Generate a fresh agent — new Solana keypair + new EVM keypair.
377
- * Use `fromSeed()` to derive both from a single backup phrase instead.
375
+ * Load an existing local identity without creating or replacing a wallet.
376
+ * Priority: SEED_HASH → ./aifinpay/agents.json → AIFINPAY_AGENT_SECRET →
377
+ * the MCP keystore at AIFINPAY_HOME/agent.json (default ~/.aifinpay).
378
+ * AIFINPAY_AGENTS_FILE/AGENT_ID select a project file/record; encrypted
379
+ * keystores require AIFINPAY_WALLET_PASSPHRASE. Reads process.env, not .env.
380
+ * Missing, invalid or ambiguous configuration throws before any network call.
381
+ */
382
+ static async fromEnvironment(opts = {}) {
383
+ const identity = loadLocalWalletIdentity();
384
+ try {
385
+ return identity.seedHash !== undefined
386
+ ? await AiFinPayAgent.fromSeed(identity.seedHash, opts)
387
+ : await AiFinPayAgent.fromSolanaSecret(identity.secretB58, opts);
388
+ }
389
+ catch {
390
+ // Decoder/library errors can include input. Never echo local key material.
391
+ throw new AiFinPayError(`Invalid wallet selected from ${identity.source}; refusing to generate a replacement wallet`);
392
+ }
393
+ }
394
+ /**
395
+ * Generate a fresh, ephemeral agent. Every call selects a new identity;
396
+ * existing environment variables and keystores are NOT loaded or changed.
397
+ * This method does not persist its keys. Use fromEnvironment() for a funded
398
+ * existing wallet, or privately persist the new seed/secret before funding.
378
399
  */
379
400
  static async new(opts = {}) {
380
401
  // Seed-derived, NOT an independent random EVM key.
@@ -411,17 +432,20 @@ export class AiFinPayAgent {
411
432
  * TODO(phase-1): replace with BIP-39/BIP-44 derivation
412
433
  * m/44'/501'/0'/0' (Solana) + m/44'/60'/0'/0/0 (EVM) once the
413
434
  * wallet-import audit recommendation lands.
435
+ * An explicit opts.evmPrivateKey imports an existing EVM wallet instead;
436
+ * keep that key as well as the seed to restore the same EVM address.
414
437
  */
415
438
  static async fromSeed(seedHex, opts = {}) {
416
- const seed = hexToBytes(seedHex.replace(/^0x/, ""));
417
- if (seed.length !== 32) {
439
+ const normalizedSeed = seedHex.replace(/^0x/, "");
440
+ if (!/^[0-9a-fA-F]{64}$/.test(normalizedSeed)) {
418
441
  throw new AiFinPayError(`fromSeed: seed must be 32 bytes (64 hex chars)`);
419
442
  }
443
+ const seed = hexToBytes(normalizedSeed);
420
444
  const kp = nacl.sign.keyPair.fromSeed(seed);
421
- const inner = Agent
422
- ._ofKeyPair?.(kp, opts) ?? Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
445
+ const inner = Agent._ofKeyPair?.(kp, opts) ??
446
+ Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
423
447
  // Derive EVM key from seed (independent, not BIP-44 — see TODO above)
424
- const evmHex = ("0x" + bytesToHex(crypto32(seed)));
448
+ const evmHex = opts.evmPrivateKey ?? ("0x" + bytesToHex(crypto32(seed)));
425
449
  const evmAccount = privateKeyToAccount(evmHex);
426
450
  return new AiFinPayAgent(inner, evmAccount, opts);
427
451
  }
@@ -439,8 +463,7 @@ export class AiFinPayAgent {
439
463
  */
440
464
  static async fromSolanaSecret(secretB58, opts = {}) {
441
465
  const inner = Agent.fromSecretB58(secretB58, opts);
442
- const evmKey = opts.evmPrivateKey
443
- ?? ("0x" + bytesToHex(crypto32(inner.secretKey.subarray(0, 32))));
466
+ const evmKey = opts.evmPrivateKey ?? ("0x" + bytesToHex(crypto32(inner.secretKey.subarray(0, 32))));
444
467
  const evmAccount = privateKeyToAccount(evmKey);
445
468
  return new AiFinPayAgent(inner, evmAccount, opts);
446
469
  }
@@ -449,8 +472,12 @@ export class AiFinPayAgent {
449
472
  // Canonical: Solana pubkey for back-compat with the leaderboard / Seat PDA.
450
473
  return this.inner.address;
451
474
  }
452
- get solanaAddress() { return this.inner.address; }
453
- get evmAddress() { return this.evmAccount.address; }
475
+ get solanaAddress() {
476
+ return this.inner.address;
477
+ }
478
+ get evmAddress() {
479
+ return this.evmAccount.address;
480
+ }
454
481
  /**
455
482
  * Casper identity, derived from the same seed as the other two.
456
483
  *
@@ -466,7 +493,9 @@ export class AiFinPayAgent {
466
493
  return casperIdentityFromSeed(this.inner.secretKey.subarray(0, 32));
467
494
  }
468
495
  /** Convenience: the account hash, which is what a Casper explorer wants. */
469
- get casperAddress() { return this.casper.accountHash; }
496
+ get casperAddress() {
497
+ return this.casper.accountHash;
498
+ }
470
499
  // ── Registry ────────────────────────────────────────────────────────────
471
500
  async fetchRegistry(force = false) {
472
501
  if (this.cachedRegistry && !force)
@@ -541,9 +570,7 @@ export class AiFinPayAgent {
541
570
  description: opts.description ?? null,
542
571
  endpoint: opts.endpoint,
543
572
  capabilities: opts.capabilities ?? [],
544
- pricing: opts.pricing
545
- ? { per_call: opts.pricing.perCall, currency: opts.pricing.currency ?? "USDC" }
546
- : null,
573
+ pricing: opts.pricing ? { per_call: opts.pricing.perCall, currency: opts.pricing.currency ?? "USDC" } : null,
547
574
  nonce,
548
575
  signature,
549
576
  }),
@@ -629,9 +656,8 @@ export class AiFinPayAgent {
629
656
  if (this.ledgerOverride)
630
657
  return this.ledgerOverride;
631
658
  if (!this._ledger) {
632
- this._ledger = this.budgetCaps.daily_usd !== undefined
633
- ? FileSpendLedger.forAgent(this.evmAddress)
634
- : new MemorySpendLedger();
659
+ this._ledger =
660
+ this.budgetCaps.daily_usd !== undefined ? FileSpendLedger.forAgent(this.evmAddress) : new MemorySpendLedger();
635
661
  }
636
662
  return this._ledger;
637
663
  }
@@ -671,23 +697,6 @@ export class AiFinPayAgent {
671
697
  return false;
672
698
  throw err;
673
699
  }
674
- checkBudget(costUsd) {
675
- const mode = this.budgetCaps.on_limit_exceeded ?? "throw";
676
- if (this.budgetCaps.per_call_usd !== undefined && costUsd > this.budgetCaps.per_call_usd) {
677
- const err = new BudgetCapExceededError("per_call", `cost $${costUsd} exceeds per-call cap $${this.budgetCaps.per_call_usd}`);
678
- if (mode === "skip")
679
- return false;
680
- throw err;
681
- }
682
- const after = this.spend24h.total24h() + costUsd;
683
- if (this.budgetCaps.daily_usd !== undefined && after > this.budgetCaps.daily_usd) {
684
- const err = new BudgetCapExceededError("daily", `daily spend ${after.toFixed(4)} would exceed cap $${this.budgetCaps.daily_usd}`);
685
- if (mode === "skip")
686
- return false;
687
- throw err;
688
- }
689
- return true;
690
- }
691
700
  /**
692
701
  * Current 24-hour rolling spend across all paid calls made by this
693
702
  * agent instance. Useful for budget dashboards on the consumer side.
@@ -695,6 +704,16 @@ export class AiFinPayAgent {
695
704
  getSpend24h() {
696
705
  return this.spend24h.total24h();
697
706
  }
707
+ /**
708
+ * Durable daily spend from the budget ledger (settled + outstanding, 24h
709
+ * window). Unlike getSpend24h()'s in-memory ring, this survives reload and
710
+ * reflects reserve/commit accounting even with no signing tool attached —
711
+ * the number a long-lived host (e.g. MCP) should display. 0 when no
712
+ * daily_usd cap is set and nothing was reserved.
713
+ */
714
+ async getDailySpendUsd() {
715
+ return this.ledger.total(24 * 3600 * 1000);
716
+ }
698
717
  /**
699
718
  * Sanity-check the USD estimate of an on-chain challenge amount against
700
719
  * the declared cost and the per-call cap BEFORE signing the tx.
@@ -725,13 +744,21 @@ export class AiFinPayAgent {
725
744
  */
726
745
  priceCache;
727
746
  async nativeUsdFor(deployment) {
728
- const env = process.env[deployment.nativeUsdEnv];
747
+ return this.tokenUsd(deployment.chain.nativeCurrency.symbol.toUpperCase(), deployment.nativeUsdEnv);
748
+ }
749
+ /**
750
+ * Native-token USD price: explicit env var, then the protocol price feed,
751
+ * then NaN ("unknown") — never a stale constant. Callers decide what an
752
+ * unknown price means for their flow (balance() excludes the leg;
753
+ * guardChallengeAmount declines to block).
754
+ */
755
+ async tokenUsd(symbol, envVar) {
756
+ const env = process.env[envVar];
729
757
  if (env !== undefined) {
730
758
  const v = parseFloat(env);
731
759
  if (Number.isFinite(v) && v > 0)
732
- return { usd: v, source: deployment.nativeUsdEnv };
760
+ return { usd: v, source: envVar };
733
761
  }
734
- const symbol = deployment.chain.nativeCurrency.symbol.toUpperCase();
735
762
  const fresh = this.priceCache && Date.now() - this.priceCache.at < 60_000;
736
763
  if (!fresh) {
737
764
  try {
@@ -771,10 +798,7 @@ export class AiFinPayAgent {
771
798
  let balance;
772
799
  let gasPrice;
773
800
  try {
774
- [balance, gasPrice] = await Promise.all([
775
- publicClient.getBalance({ address }),
776
- publicClient.getGasPrice(),
777
- ]);
801
+ [balance, gasPrice] = await Promise.all([publicClient.getBalance({ address }), publicClient.getGasPrice()]);
778
802
  }
779
803
  catch {
780
804
  // An RPC that will not answer must not turn into a funding error; let
@@ -795,7 +819,7 @@ export class AiFinPayAgent {
795
819
  return s.includes(".") ? s.replace(/(\.\d{6})\d+$/, "$1").replace(/\.?0+$/, "") || "0" : s;
796
820
  };
797
821
  const { usd } = await this.nativeUsdFor(deployment);
798
- const toUsd = (wei) => Number.isFinite(usd) ? (Number(wei) / 1e18) * usd : Number.NaN;
822
+ const toUsd = (wei) => (Number.isFinite(usd) ? (Number(wei) / 1e18) * usd : Number.NaN);
799
823
  throw new InsufficientFundsError(toUsd(needed), toUsd(balance), `Agent ${address} holds ${short(balance)} ${symbol} on ${chainName}, but this call needs ` +
800
824
  `about ${short(needed)} ${symbol} — ${short(valueWei)} for the payment and up to ` +
801
825
  `${short(gasBudget)} for gas. The gas figure is a ceiling: it assumes 200k gas at the ` +
@@ -909,8 +933,8 @@ export class AiFinPayAgent {
909
933
  async bridgeQuote(opts) {
910
934
  const fromToken = opts.fromToken ?? USDC_NATIVE[opts.fromChain];
911
935
  const toToken = opts.toToken ?? USDC_NATIVE[opts.toChain];
912
- const fromAmount = opts.fromAmount
913
- ?? (opts.amount_usdc !== undefined
936
+ const fromAmount = opts.fromAmount ??
937
+ (opts.amount_usdc !== undefined
914
938
  ? Math.round(opts.amount_usdc * 1e6).toString() // USDC has 6 decimals
915
939
  : undefined);
916
940
  if (!fromAmount) {
@@ -948,14 +972,10 @@ export class AiFinPayAgent {
948
972
  }
949
973
  // ── Call ────────────────────────────────────────────────────────────────
950
974
  /**
951
- * High-level paid call. Resolves the provider, picks a chain, builds the
952
- * payment, sends to the bridge, retries with payment proof.
953
- *
954
- * EVM settlement: per-call splitter via on-chain `B2BSplitter.payMatic()`
955
- * (generic native-token entrypoint) on any SPLITTER_DEPLOYMENTS chain —
956
- * polygon (default), base, optimism, unichain, botchain, xrplevm. Pass
957
- * `chain` in CallOptions to override (the provider must accept it).
958
- * Solana settlement: b2b_pay_with_split (live since 2026-05-18).
975
+ * Legacy bridge call. Free/read-only bridge responses are passed through;
976
+ * paid 402 challenges fail closed because this legacy settlement surface has
977
+ * no independently reviewed target. Use the reviewed fetchPaid v1.3 route
978
+ * with a fresh trusted quote/FX result for paid requests.
959
979
  *
960
980
  * Returns `null` (instead of a `Response`) iff a budget cap was hit
961
981
  * AND budget.on_limit_exceeded is set to "skip" — the call is dropped
@@ -975,7 +995,7 @@ export class AiFinPayAgent {
975
995
  const reservation = await this.reserveDaily(cost);
976
996
  if (reservation === "skip")
977
997
  return null;
978
- let settled = false;
998
+ const settled = false;
979
999
  try {
980
1000
  if (provider.mode === "session") {
981
1001
  throw new AiFinPayError(`Provider ${provider.name} requires session mode — use openSession() (phase-3 feature)`);
@@ -1000,8 +1020,7 @@ export class AiFinPayAgent {
1000
1020
  headers: { "content-type": "application/json", ...extraHeaders },
1001
1021
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
1002
1022
  // CallOptions.timeoutMs was previously declared but never applied.
1003
- signal: opts.signal
1004
- ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined),
1023
+ signal: opts.signal ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined),
1005
1024
  });
1006
1025
  // 1. Initial unauthenticated POST → expect 402 challenge from the bridge.
1007
1026
  const fullUrl = url.replace(/\/$/, "") + path;
@@ -1020,119 +1039,22 @@ export class AiFinPayAgent {
1020
1039
  catch (e) {
1021
1040
  throw new X402Error(`bridge returned 402 with non-JSON body`);
1022
1041
  }
1023
- // ── Solana branch (b2b_pay_with_split atomic split, live 2026-05-18) ──
1024
- if (chain === "solana") {
1025
- if (!challenge.pay_solana) {
1026
- throw new X402Error(`Bridge ${provider.name} returned 402 without a pay_solana block. ` +
1027
- `Either pick chain: "polygon" or ask the operator to set ` +
1028
- `BRIDGE_MERCHANT_SOLANA on the bridge service.`);
1029
- }
1030
- const ps = challenge.pay_solana;
1031
- // Guard: the bridge controls the challenge — verify the demanded
1032
- // amount is in the same ballpark as the declared cost / caps before
1033
- // signing anything. Stops a misquoting bridge from draining the agent.
1034
- {
1035
- const lamports = Number(ps.total_lamports ?? ps.merchant_amount_lamports);
1036
- const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
1037
- const estUsd = (Number.isFinite(lamports) ? lamports / 1e9 : 0) * solUsd;
1038
- const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
1039
- if (!guarded)
1040
- return null;
1041
- }
1042
- const solTxSig = await this.submitSolanaB2BPayWithSplit(ps);
1043
- const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
1044
- "x-solana-tx": solTxSig,
1045
- "x-order-id": ps.order_id,
1046
- }));
1047
- if (!paidResp.ok) {
1048
- const detail = await paidResp.text().catch(() => "<unreadable>");
1049
- throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after Solana payment ${solTxSig}: ${detail.slice(0, 300)}`);
1050
- }
1051
- // Money moved here, and only here. The reservation becomes a
1052
- // settled amount; the finally below then has nothing to release.
1053
- this.spend24h.add(cost);
1054
- if (typeof reservation === "string") {
1055
- await this.ledger.commit(reservation, cost);
1056
- }
1057
- settled = true;
1058
- if (this.telemetry)
1059
- this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: solTxSig });
1060
- // @internal — attach settlement metadata for consumers (MCP, telemetry
1061
- // dashboards) that need to surface the tx hash without an extra RPC
1062
- // call. Response headers are read-only after construction so we use
1063
- // an instance property + cast on the read side.
1064
- paidResp.aifinpayTx = solTxSig;
1065
- paidResp.aifinpayChain = "solana";
1066
- return paidResp;
1067
- }
1068
- // ── EVM branch (B2BSplitter.payMatic — generic native-token atomic split) ──
1069
- // Settles on any chain in SPLITTER_DEPLOYMENTS (polygon default; base,
1070
- // optimism, unichain, botchain, xrplevm). Native token only — the
1071
- // deployed splitter payment path has no ERC-20/USDC entrypoint.
1072
- const pm = nativePayBlock(challenge);
1073
- if (!pm) {
1074
- throw new X402Error(`bridge ${provider.name} returned 402 with neither a pay_native nor a pay_matic block — it is speaking a protocol this SDK does not implement (fields: ${Object.keys(challenge).join(", ")})`);
1042
+ // Legacy bridge settlement is fail-closed. The challenge historically
1043
+ // supplied arbitrary splitter/program addresses and native amounts, so
1044
+ // continuing from here could sign against an unreviewed target or let a
1045
+ // missing price bypass the budget. Keep free/read-only calls above, but
1046
+ // refuse every paid legacy challenge before any signing or broadcast.
1047
+ if (!(typeof provider.price_usd === "number" && Number.isFinite(provider.price_usd) && provider.price_usd > 0) ||
1048
+ !(typeof cost === "number" && Number.isFinite(cost) && cost > 0)) {
1049
+ throw new AiFinPayError(`legacy call() refused ${provider.name}: the provider has no trusted positive USD price; ` +
1050
+ `use the reviewed fetchPaid v1.3 route with fresh trusted FX instead`);
1075
1051
  }
1076
- const deployment = SPLITTER_DEPLOYMENTS[chain];
1077
- if (!deployment) {
1078
- throw new AiFinPayError(`No B2BSplitter deployment registered for chain "${chain}" — supported: ${Object.keys(SPLITTER_DEPLOYMENTS).join(", ")}`);
1052
+ if (!legacySettlementIsReviewed()) {
1053
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1054
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1079
1055
  }
1080
- // Refuse to pay a quote denominated for another chain's native token:
1081
- // total_wei quoted for POL on Polygon would be a massive overpay if
1082
- // blindly re-sent as ETH on Base. Legacy bridges always emit "polygon".
1083
- if (pm.chain && pm.chain !== chain) {
1084
- throw new X402Error(`bridge ${provider.name} quoted a native payment for chain "${pm.chain}" but the call was routed to "${chain}" — refusing cross-denomination payment`);
1085
- }
1086
- // Guard: same ballpark check as the Solana branch — never sign for an
1087
- // amount wildly above the declared cost / per-call cap. Native-token
1088
- // USD price per chain via SPLITTER_DEPLOYMENTS.nativeUsdEnv.
1089
- {
1090
- const wei = Number(pm.total_wei);
1091
- const { usd: nativeUsd } = await this.nativeUsdFor(deployment);
1092
- // NaN when no price is known — guardChallengeAmount then declines to
1093
- // block rather than blocking on a guess. See nativeUsdFor().
1094
- const estUsd = (Number.isFinite(wei) ? wei / 1e18 : 0) * nativeUsd;
1095
- const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
1096
- if (!guarded)
1097
- return null;
1098
- }
1099
- // 2. Submit the B2BSplitter payment on the selected chain's mainnet and
1100
- // wait for it to land. Prefer the challenge's splitter address (current
1101
- // bridge behaviour); fall back to the registry address for bridges that
1102
- // omit it.
1103
- const splitterAddress = pm.splitter
1104
- ?? deployment.splitter;
1105
- const txHash = await this.settleSplitterNative({
1106
- chain,
1107
- splitter: splitterAddress,
1108
- splitterVersion: pm.splitter_version,
1109
- merchantWallet: pm.merchant_wallet,
1110
- totalWei: BigInt(pm.total_wei),
1111
- orderId: pm.order_id,
1112
- ipCreator: pm.ip_creator,
1113
- });
1114
- // 3. Retry the bridge with payment proof.
1115
- const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
1116
- "x-tx-hash": txHash,
1117
- "x-order-id": pm.order_id,
1118
- }));
1119
- if (!paidResp.ok) {
1120
- const detail = await paidResp.text().catch(() => "<unreadable>");
1121
- throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after on-chain payment ${txHash}: ${detail.slice(0, 300)}`);
1122
- }
1123
- // Money moved here, and only here. The reservation becomes a
1124
- // settled amount; the finally below then has nothing to release.
1125
- this.spend24h.add(cost);
1126
- if (typeof reservation === "string") {
1127
- await this.ledger.commit(reservation, cost);
1128
- }
1129
- settled = true;
1130
- if (this.telemetry)
1131
- this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: txHash });
1132
- // @internal — see Solana branch above for rationale on property-attach.
1133
- paidResp.aifinpayTx = txHash;
1134
- paidResp.aifinpayChain = chain;
1135
- return paidResp;
1056
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1057
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1136
1058
  }
1137
1059
  finally {
1138
1060
  // Every path that did not move money gives the budget back now rather
@@ -1156,87 +1078,14 @@ export class AiFinPayAgent {
1156
1078
  // a reverted transaction would otherwise be handed to a merchant as proof of
1157
1079
  // a payment that never moved.
1158
1080
  async settleSplitterNative(p) {
1159
- // call() has already checked this for the bridge flow; repeated here
1160
- // because settlement is now reachable without going through call().
1161
- const deployment = SPLITTER_DEPLOYMENTS[p.chain];
1162
- if (!deployment) {
1163
- throw new AiFinPayError(`No B2BSplitter deployment registered for chain "${p.chain}" — supported: ${Object.keys(SPLITTER_DEPLOYMENTS).join(", ")}`);
1164
- }
1165
- // ipCreator routing: the caller's explicit ip_creator, or address(0).
1166
- //
1167
- // This used to fall back to the splitter's own treasury, on the reasoning
1168
- // that address(0) "would skip the transfer and permanently strand the 1bp
1169
- // inside B2BSplitter — the contract has no sweep function". That premise is
1170
- // false. B2BSplitter._split(), lines 238-244:
1171
- //
1172
- // if (_ipCreator != address(0)) { ipAmt = _total * ipCreatorBps / D; }
1173
- // // else: ipAmt stays 0 and is absorbed into merchantAmt below
1174
- // merchantAmt = _total - treasuryAmt - ipAmt;
1175
- //
1176
- // With address(0) the royalty is not stranded — it is folded into the
1177
- // MERCHANT's leg, which is where it belongs when no creator was named. The
1178
- // fallback therefore took 1bp off every payment whose merchant had not set
1179
- // a creator, and paid it to OUR treasury.
1180
- //
1181
- // Fixed in the Python client on 2026-08-27 and not ported here. That is the
1182
- // third instance this week of one implementation being corrected and its
1183
- // twin left alone; the others were gate/src/scope.ts and this file.
1184
- const ipCreator = p.ipCreator ?? "0x0000000000000000000000000000000000000000";
1185
- const { publicClient, walletClient } = this.splitterClients(p.chain);
1186
- // The entrypoint follows the deployed contract, not the SDK release: v1.2
1187
- // (Polygon, Optimism, BOT Chain, XRPL EVM as of 2026-07-31) takes a bytes32
1188
- // paymentId and rejects one it has already settled, while Base and Unichain
1189
- // still run v1.1. Sending v1.2 calldata to a v1.1 contract reverts with no
1190
- // useful reason, so this must be decided per chain.
1191
- // An explicit `splitterVersion` wins — a server that states it knows what
1192
- // it deployed. What it must NOT fall back to is this registry's
1193
- // chain -> version entry, because the ADDRESS came from the server and
1194
- // the version would come from a table: two sources that can disagree.
1195
- //
1196
- // In production they did. The Exa bridge still hands out the pre-v1.2
1197
- // splitter 0xE34Fc0E6… and sends no version; the table says "polygon is
1198
- // 1.2"; the SDK called payNative on a contract that only has payMatic, and
1199
- // the payment reverted with nothing in the reason to explain it.
1200
- //
1201
- // So when the server is silent, ask the contract at the address we were
1202
- // actually handed. The registry is used only if the chain cannot be read.
1203
- const splitterVersion = p.splitterVersion
1204
- ?? await this.detectSplitterVersion(p.splitter, p.chain, deployment.version);
1205
- await this.assertCanAffordNative(publicClient, deployment, p.totalWei);
1206
- const txHash = splitterVersion === "1.2"
1207
- ? await walletClient.writeContract({
1208
- address: p.splitter,
1209
- abi: SPLITTER_PAY_NATIVE_ABI,
1210
- functionName: "payNative",
1211
- args: [
1212
- paymentIdFor(p.orderId),
1213
- p.merchantWallet,
1214
- ipCreator,
1215
- p.orderId,
1216
- ],
1217
- value: p.totalWei,
1218
- chain: deployment.chain,
1219
- account: this.evmAccount,
1220
- })
1221
- : await walletClient.writeContract({
1222
- address: p.splitter,
1223
- abi: SPLITTER_PAY_MATIC_ABI,
1224
- functionName: "payMatic",
1225
- args: [
1226
- p.merchantWallet,
1227
- ipCreator,
1228
- p.orderId,
1229
- ],
1230
- value: p.totalWei,
1231
- chain: deployment.chain,
1232
- account: this.evmAccount,
1233
- });
1234
- // Wait for receipt (inclusion is enough on these fast-block chains).
1235
- const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1236
- if (receipt.status !== "success") {
1237
- throw new AiFinPayError(`${deployment.chain.name} tx reverted: ${txHash}`);
1238
- }
1239
- return txHash;
1081
+ void p;
1082
+ throw new AiFinPayError("legacy splitter settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1083
+ }
1084
+ // Legacy Solana settlement remains a named private surface only so older
1085
+ // callers fail with a clear migration error instead of reaching a signer.
1086
+ async submitSolanaB2BPayWithSplit(ps) {
1087
+ void ps;
1088
+ throw new AiFinPayError("legacy Solana settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1240
1089
  }
1241
1090
  // ── AIFP-1 merchant paywall (gateway.aifinpay.io) ───────────────────────
1242
1091
  //
@@ -1252,16 +1101,43 @@ export class AiFinPayAgent {
1252
1101
  * test asserting that a receipt was reused, both need to see it. The entries
1253
1102
  * contain bearer JWTs — treat them like keys, and do not log them.
1254
1103
  */
1255
- get aifp1Receipts() { return this._aifp1Cache; }
1104
+ get aifp1Receipts() {
1105
+ return this._aifp1Cache;
1106
+ }
1256
1107
  /**
1257
- * Legacy fetchPaid cannot safely execute a v1.3 quote yet: it has no
1258
- * independently pinned runtime hash/profile input. Keep it fail-closed until
1259
- * it is wired through SettlementClient + executeSettlementInvoice.
1260
- *
1261
- * Tests replace this method with a chain stub; production never signs here.
1108
+ * Redacted receipt-cache view (no bearer JWTs): what batches this instance
1109
+ * holds, for shutdown decisions and dashboards. Safe to log and to expose
1110
+ * over MCP — use this, never `aifp1Receipts.list()`.
1262
1111
  */
1263
- async settleAifp1NativeV13(_p) {
1264
- throw new AiFinPayError("AIFP-1 fetchPaid settlement is disabled until a trusted v1.3 deployment/runtime profile is supplied; use SettlementClient with an independent TrustedSettlementRoutePin");
1112
+ getReceiptCacheSummary() {
1113
+ return this._aifp1Cache.summary();
1114
+ }
1115
+ /**
1116
+ * Use the canonical invoice executor only with a caller-supplied deployment
1117
+ * pin. An unauthenticated quote cannot establish its own signing authority.
1118
+ */
1119
+ async settleAifp1NativeV13(p, opts) {
1120
+ const pin = opts.settlementPin;
1121
+ if (!pin || pin.route_class !== "AIFP-1" || pin.chain !== "polygon" || pin.testnet) {
1122
+ throw new AiFinPayError("AIFP-1 fetchPaid requires an independent trusted Polygon v1.3 settlement pin; other receipt networks are not supported");
1123
+ }
1124
+ const client = new SettlementClient({
1125
+ baseUrl: opts.apiBaseUrl ?? "https://api.aifinpay.io",
1126
+ fetchImpl: this.inner.fetchImpl,
1127
+ timeoutMs: this.inner.timeoutMs,
1128
+ });
1129
+ const invoice = await client.invoice({
1130
+ route_class: "AIFP-1",
1131
+ chain: "polygon",
1132
+ asset: "POL",
1133
+ gross_amount: p.grossWei,
1134
+ merchant_wallet: p.merchantWallet,
1135
+ order_id: p.orderId,
1136
+ valid_until: Number(p.validUntil),
1137
+ });
1138
+ const { publicClient, walletClient } = this.polygonClients();
1139
+ const execution = await executeSettlementInvoice(invoice, walletClient, publicClient, pin);
1140
+ return execution.settlement_tx;
1265
1141
  }
1266
1142
  /**
1267
1143
  * Fetch a paywalled URL, paying the AIFP-1 gateway if it asks.
@@ -1285,7 +1161,9 @@ export class AiFinPayAgent {
1285
1161
  // (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
1286
1162
  // would silently opt the agent out of its owner's own limits.
1287
1163
  agentId: opts.agentId ?? this.evmAddress,
1288
- settle: (p) => this.settleAifp1NativeV13(p),
1164
+ payerAddress: this.evmAddress,
1165
+ signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
1166
+ settle: (p) => this.settleAifp1NativeV13(p, opts),
1289
1167
  checkPerCall: (usd) => this.checkPerCall(usd),
1290
1168
  reserveDaily: (usd) => this.reserveDaily(usd),
1291
1169
  commit: (id, usd) => this.ledger.commit(id, usd),
@@ -1294,77 +1172,57 @@ export class AiFinPayAgent {
1294
1172
  this.spend24h.add(amountUsd);
1295
1173
  if (this.telemetry) {
1296
1174
  this.reportTelemetry({
1297
- kind: "aifp1", merchant: merchantId, chain: "polygon",
1298
- cost: amountUsd, tx: txRef,
1175
+ kind: "aifp1",
1176
+ merchant: merchantId,
1177
+ chain: "polygon",
1178
+ cost: amountUsd,
1179
+ tx: txRef,
1299
1180
  });
1300
1181
  }
1301
1182
  },
1302
1183
  };
1303
1184
  return aifp1Fetch(deps, url, init, opts);
1304
1185
  }
1305
- // ── Solana b2b_pay_with_split — build, sign, send via @solana/web3.js ───
1306
- //
1307
- // Manual instruction encoding (no Anchor dep):
1308
- // discriminator = sha256("global:b2b_pay_with_split")[:8]
1309
- // args (Borsh) = u64 merchant_amount_lamports + string order_id
1310
- // accounts = [config_pda, vault_pda, agent (signer), treasury,
1311
- // ip_creator, merchant_wallet, system_program]
1312
- // PDAs derived from program_id with seeds ["config"] and ["vault"].
1313
- async submitSolanaB2BPayWithSplit(ps) {
1314
- const conn = new Connection(this.solanaRpc, "confirmed");
1315
- const programId = new PublicKey(ps.program_id);
1316
- const merchant = new PublicKey(ps.merchant_wallet);
1317
- const treasury = new PublicKey(ps.treasury);
1318
- // ip_creator slot: not surfaced by current bridge 402s; route through
1319
- // treasury so the on-chain 1bp still settles atomically (treasury earns
1320
- // both 100bp + 1bp). When bridges add per-merchant ip_creator support,
1321
- // accept it from `ps` and pass through.
1322
- const ipCreator = treasury;
1323
- // Solana keypair from legacy Agent inner (tweetnacl 64-byte secret).
1324
- const kp = Keypair.fromSecretKey(this.inner.secretKey);
1325
- if (kp.publicKey.toString() !== this.inner.address) {
1326
- throw new AiFinPayError(`Internal: Solana keypair pubkey ${kp.publicKey.toString()} does not match agent address ${this.inner.address}`);
1327
- }
1328
- // PDAs
1329
- const [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
1330
- const [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], programId);
1331
- // Instruction discriminator: Anchor convention sha256("global:<fn_name>")[:8].
1332
- const disc = createHash("sha256").update("global:b2b_pay_with_split").digest().subarray(0, 8);
1333
- // Borsh args: merchant_amount_lamports (u64 LE) + order_id (string = u32 len + utf8)
1334
- const merchantAmount = BigInt(ps.merchant_amount_lamports);
1335
- const amountBuf = Buffer.alloc(8);
1336
- amountBuf.writeBigUInt64LE(merchantAmount);
1337
- const orderBytes = Buffer.from(ps.order_id, "utf8");
1338
- if (orderBytes.length > 64) {
1339
- throw new AiFinPayError(`order_id too long (${orderBytes.length} bytes > 64 limit)`);
1340
- }
1341
- const orderLenBuf = Buffer.alloc(4);
1342
- orderLenBuf.writeUInt32LE(orderBytes.length);
1343
- const data = Buffer.concat([disc, amountBuf, orderLenBuf, orderBytes]);
1344
- const ix = new TransactionInstruction({
1345
- programId,
1346
- keys: [
1347
- { pubkey: configPda, isSigner: false, isWritable: false },
1348
- { pubkey: vaultPda, isSigner: false, isWritable: false },
1349
- { pubkey: kp.publicKey, isSigner: true, isWritable: true },
1350
- { pubkey: treasury, isSigner: false, isWritable: true },
1351
- { pubkey: ipCreator, isSigner: false, isWritable: true },
1352
- { pubkey: merchant, isSigner: false, isWritable: true },
1353
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1354
- ],
1355
- data,
1186
+ /**
1187
+ * List verified settlement routes. Non-signing: fetches + returns the
1188
+ * backend route table (same client the MCP `settlement_routes` tool should
1189
+ * use instead of its own inline HTTP). Never signs or broadcasts.
1190
+ */
1191
+ async settlementRoutes(routeClass, baseUrl) {
1192
+ return this.settlementClient(baseUrl).routes(routeClass);
1193
+ }
1194
+ /**
1195
+ * Request a settlement invoice and validate it (canonical split,
1196
+ * chain binding, request match). Non-signing: returns the invoice for
1197
+ * display/approval. Never signs or broadcasts.
1198
+ */
1199
+ async requestSettlementInvoice(input, baseUrl) {
1200
+ return this.settlementClient(baseUrl).invoice(input);
1201
+ }
1202
+ settlementClient(baseUrl) {
1203
+ return new SettlementClient({
1204
+ baseUrl: baseUrl ?? "https://api.aifinpay.io",
1205
+ fetchImpl: this.inner.fetchImpl,
1206
+ timeoutMs: this.inner.timeoutMs,
1356
1207
  });
1357
- const tx = new Transaction().add(ix);
1358
- // sendAndConfirmTransaction sets recentBlockhash + feePayer + signs.
1359
- const sig = await sendAndConfirmTransaction(conn, tx, [kp], {
1360
- commitment: "confirmed",
1361
- preflightCommitment: "confirmed",
1208
+ }
1209
+ /**
1210
+ * Prepaid quota batches for this agent's address, most room first with a
1211
+ * per-merchant rollup. Non-signing read of retained receipts metadata.
1212
+ */
1213
+ async getQuota(opts = {}) {
1214
+ return getQuota({
1215
+ address: this.evmAddress,
1216
+ merchantId: opts.merchantId,
1217
+ includeExhausted: opts.includeExhausted,
1218
+ baseUrl: opts.baseUrl,
1219
+ fetchImpl: this.inner.fetchImpl,
1362
1220
  });
1363
- return sig;
1364
1221
  }
1365
1222
  // ── Sessions ────────────────────────────────────────────────────────────
1366
1223
  /**
1367
1224
  * TODO(phase-3): open a metered budget session.
1225
+ * @deprecated Not implemented — always throws. Tracked for phase 3; do not call.
1368
1226
  */
1369
1227
  async openSession(_args) {
1370
1228
  throw new AiFinPayError("openSession() not implemented yet — see migration phase 3");
@@ -1373,24 +1231,32 @@ export class AiFinPayAgent {
1373
1231
  /**
1374
1232
  * Snapshot the agent's funds across both chains, USD-normalised.
1375
1233
  *
1376
- * Phase 1.4: native MATIC + native SOL via RPC; ERC-20 / SPL token balances
1377
- * deferred (we don't yet need them for the unified `call()` flow which
1378
- * settles in MATIC). Pyth feeds are TODO; for now uses
1379
- * `AIFINPAY_MATIC_USD` and `AIFINPAY_SOL_USD` env, defaulting to 0.7 and 200.
1234
+ * Native prices are env-first (`AIFINPAY_MATIC_USD` / `AIFINPAY_SOL_USD`),
1235
+ * then the protocol price feed — never a stale compiled-in constant. A leg
1236
+ * with no known price is excluded from the total and listed in
1237
+ * `unknown_legs` instead of being fabricated.
1380
1238
  */
1381
1239
  async balance() {
1382
- const maticUsd = parseFloat(process.env.AIFINPAY_MATIC_USD ?? "0.70");
1383
- const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
1240
+ const [pol, sol] = await Promise.all([
1241
+ this.tokenUsd("POL", SPLITTER_DEPLOYMENTS.polygon.nativeUsdEnv),
1242
+ this.tokenUsd("SOL", "AIFINPAY_SOL_USD"),
1243
+ ]);
1384
1244
  const polygon = await this.fetchPolygonNative().catch(() => 0);
1385
1245
  const solana = await this.fetchSolanaNative().catch(() => 0);
1386
1246
  const solana_usdc = await this.fetchSolanaUsdc().catch(() => 0);
1387
1247
  const polygon_usdc = await this.fetchPolygonUsdc().catch(() => 0);
1388
- const polygonUsd = polygon * maticUsd;
1389
- const solanaUsd = solana * solUsd;
1390
- const solanaUsdcUsd = solana_usdc; // USDC ≈ $1
1391
- const polygonUsdcUsd = polygon_usdc; // USDC ≈ $1
1248
+ const unknown_legs = [];
1249
+ let agent_balance_usd = solana_usdc + polygon_usdc; // USDC ≈ $1
1250
+ if (Number.isFinite(pol.usd))
1251
+ agent_balance_usd += polygon * pol.usd;
1252
+ else
1253
+ unknown_legs.push("pol");
1254
+ if (Number.isFinite(sol.usd))
1255
+ agent_balance_usd += solana * sol.usd;
1256
+ else
1257
+ unknown_legs.push("sol");
1392
1258
  return {
1393
- agent_balance_usd: polygonUsd + solanaUsd + solanaUsdcUsd + polygonUsdcUsd,
1259
+ agent_balance_usd,
1394
1260
  chains: {
1395
1261
  solana: { sol: solana, usdc: solana_usdc, msecco_balance: 0 },
1396
1262
  polygon: { matic: polygon, usdc: polygon_usdc, msecco_balance: 0 },
@@ -1398,6 +1264,8 @@ export class AiFinPayAgent {
1398
1264
  spend_24h_usd: this.spend24h.total24h(),
1399
1265
  budget_caps: this.budgetCaps,
1400
1266
  priced_at: Math.floor(Date.now() / 1000),
1267
+ prices: { pol, sol },
1268
+ ...(unknown_legs.length ? { unknown_legs } : {}),
1401
1269
  };
1402
1270
  }
1403
1271
  async fetchPolygonNative() {
@@ -1444,11 +1312,7 @@ export class AiFinPayAgent {
1444
1312
  jsonrpc: "2.0",
1445
1313
  id: 1,
1446
1314
  method: "getTokenAccountsByOwner",
1447
- params: [
1448
- this.solanaAddress,
1449
- { mint: USDC_SOLANA_MINT },
1450
- { encoding: "jsonParsed" },
1451
- ],
1315
+ params: [this.solanaAddress, { mint: USDC_SOLANA_MINT }, { encoding: "jsonParsed" }],
1452
1316
  }),
1453
1317
  });
1454
1318
  if (!r.ok)
@@ -1486,8 +1350,8 @@ export class AiFinPayAgent {
1486
1350
  * six registry chains against mainnet.
1487
1351
  *
1488
1352
  * One eth_getCode, cached per (chain, address). Falls back to the registry
1489
- * only when the code cannot be read: guessing beats refusing to pay, and for
1490
- * the addresses we deployed the guess is right.
1353
+ * only for this read-only diagnostic when code cannot be read. The legacy
1354
+ * payment path is disabled regardless of the reported or detected version.
1491
1355
  */
1492
1356
  splitterVersionCache = new Map();
1493
1357
  async detectSplitterVersion(splitter, chainName, fallback) {
@@ -1533,6 +1397,7 @@ export class AiFinPayAgent {
1533
1397
  // ── Reputation ──────────────────────────────────────────────────────────
1534
1398
  /**
1535
1399
  * TODO(phase-4 / phase-5): query operator backend reputation engine.
1400
+ * @deprecated Zero-stub — returns unverified zeros until the engine lands; do not treat as signal.
1536
1401
  */
1537
1402
  async reputation() {
1538
1403
  return {
@@ -1546,13 +1411,19 @@ export class AiFinPayAgent {
1546
1411
  // ── Telemetry (opt-out) ─────────────────────────────────────────────────
1547
1412
  reportTelemetry(payload) {
1548
1413
  // Fire-and-forget. No body content, only metadata.
1549
- void this.inner.fetchImpl(`${this.inner.baseUrl}/api/telemetry`, {
1414
+ void this.inner
1415
+ .fetchImpl(`${this.inner.baseUrl}/api/telemetry`, {
1550
1416
  method: "POST",
1551
1417
  headers: { "content-type": "application/json" },
1552
1418
  body: JSON.stringify({ agent_id: this.id, ...payload, ts: Date.now() }),
1553
- }).catch(() => { });
1419
+ })
1420
+ .catch(() => { });
1554
1421
  }
1555
1422
  }
1423
+ /** Legacy bridge settlement has no reviewed mainnet route; keep this closed. */
1424
+ function legacySettlementIsReviewed() {
1425
+ return false;
1426
+ }
1556
1427
  // ── Internal hex helpers (small, no extra deps) ────────────────────────────
1557
1428
  function hexToBytes(hex) {
1558
1429
  const out = new Uint8Array(hex.length / 2);