@aifinpay/agent 2.0.0-rc.8 → 2.0.3

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 (123) 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 +1 -0
  29. package/dist/crossChain.d.ts.map +1 -1
  30. package/dist/crossChain.js +87 -5
  31. package/dist/crossChain.js.map +1 -1
  32. package/dist/crypto.d.ts.map +1 -1
  33. package/dist/crypto.js.map +1 -1
  34. package/dist/deploymentResolver.d.ts +95 -0
  35. package/dist/deploymentResolver.d.ts.map +1 -0
  36. package/dist/deploymentResolver.js +169 -0
  37. package/dist/deploymentResolver.js.map +1 -0
  38. package/dist/errors.d.ts +17 -0
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +14 -0
  41. package/dist/errors.js.map +1 -1
  42. package/dist/facilitators/aifinpay.d.ts +4 -5
  43. package/dist/facilitators/aifinpay.d.ts.map +1 -1
  44. package/dist/facilitators/aifinpay.js +50 -21
  45. package/dist/facilitators/aifinpay.js.map +1 -1
  46. package/dist/facilitators/base.d.ts +12 -1
  47. package/dist/facilitators/base.d.ts.map +1 -1
  48. package/dist/facilitators/coinbase.d.ts.map +1 -1
  49. package/dist/facilitators/coinbase.js +3 -7
  50. package/dist/facilitators/coinbase.js.map +1 -1
  51. package/dist/facilitators/detect.d.ts.map +1 -1
  52. package/dist/facilitators/detect.js +2 -7
  53. package/dist/facilitators/detect.js.map +1 -1
  54. package/dist/facilitators/standard-x402.d.ts.map +1 -1
  55. package/dist/facilitators/standard-x402.js +40 -30
  56. package/dist/facilitators/standard-x402.js.map +1 -1
  57. package/dist/generated/solanaV14Deployments.generated.d.ts +25 -0
  58. package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -0
  59. package/dist/generated/solanaV14Deployments.generated.js +39 -0
  60. package/dist/generated/solanaV14Deployments.generated.js.map +1 -0
  61. package/dist/generated/splitterRoutes.generated.d.ts +24 -0
  62. package/dist/generated/splitterRoutes.generated.d.ts.map +1 -0
  63. package/dist/generated/splitterRoutes.generated.js +453 -0
  64. package/dist/generated/splitterRoutes.generated.js.map +1 -0
  65. package/dist/generated/v14Deployments.generated.d.ts +49 -0
  66. package/dist/generated/v14Deployments.generated.d.ts.map +1 -0
  67. package/dist/generated/v14Deployments.generated.js +415 -0
  68. package/dist/generated/v14Deployments.generated.js.map +1 -0
  69. package/dist/index.d.ts +27 -13
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +16 -7
  72. package/dist/index.js.map +1 -1
  73. package/dist/localIdentity.d.ts +17 -0
  74. package/dist/localIdentity.d.ts.map +1 -0
  75. package/dist/localIdentity.js +95 -0
  76. package/dist/localIdentity.js.map +1 -0
  77. package/dist/settlement.d.ts +35 -5
  78. package/dist/settlement.d.ts.map +1 -1
  79. package/dist/settlement.js +232 -76
  80. package/dist/settlement.js.map +1 -1
  81. package/dist/settlementHttp.d.ts +10 -0
  82. package/dist/settlementHttp.d.ts.map +1 -0
  83. package/dist/settlementHttp.js +78 -0
  84. package/dist/settlementHttp.js.map +1 -0
  85. package/dist/settlementV14.d.ts +10 -9
  86. package/dist/settlementV14.d.ts.map +1 -1
  87. package/dist/settlementV14.js +43 -55
  88. package/dist/settlementV14.js.map +1 -1
  89. package/dist/solanaDeploymentResolver.d.ts +98 -0
  90. package/dist/solanaDeploymentResolver.d.ts.map +1 -0
  91. package/dist/solanaDeploymentResolver.js +162 -0
  92. package/dist/solanaDeploymentResolver.js.map +1 -0
  93. package/dist/solanaV14Deployments.generated.d.ts +25 -0
  94. package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
  95. package/dist/solanaV14Deployments.generated.js +39 -0
  96. package/dist/solanaV14Deployments.generated.js.map +1 -0
  97. package/dist/spendLedger.d.ts.map +1 -1
  98. package/dist/spendLedger.js +1 -3
  99. package/dist/spendLedger.js.map +1 -1
  100. package/dist/splitterRoutes.d.ts +11 -4
  101. package/dist/splitterRoutes.d.ts.map +1 -1
  102. package/dist/splitterRoutes.generated.d.ts +11 -9
  103. package/dist/splitterRoutes.generated.d.ts.map +1 -1
  104. package/dist/splitterRoutes.generated.js +72 -18
  105. package/dist/splitterRoutes.generated.js.map +1 -1
  106. package/dist/splitterRoutes.js +7 -5
  107. package/dist/splitterRoutes.js.map +1 -1
  108. package/dist/unifiedAgent.d.ts +97 -29
  109. package/dist/unifiedAgent.d.ts.map +1 -1
  110. package/dist/unifiedAgent.js +232 -349
  111. package/dist/unifiedAgent.js.map +1 -1
  112. package/dist/v14Deployments.generated.d.ts +49 -0
  113. package/dist/v14Deployments.generated.d.ts.map +1 -0
  114. package/dist/v14Deployments.generated.js +413 -0
  115. package/dist/v14Deployments.generated.js.map +1 -0
  116. package/dist/wallet.d.ts.map +1 -1
  117. package/dist/wallet.js +2 -2
  118. package/dist/wallet.js.map +1 -1
  119. package/package.json +27 -6
  120. package/dist/cli.d.ts +0 -3
  121. package/dist/cli.d.ts.map +0 -1
  122. package/dist/cli.js +0 -127
  123. 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
- 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";
19
+ import { botchain, xrplevm, robinhood } from "./chains.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,
@@ -143,6 +146,17 @@ export const SPLITTER_DEPLOYMENTS = {
143
146
  nativeUsdEnv: "AIFINPAY_BOT_USD",
144
147
  nativeUsdDefault: 1, // no reliable public feed; set the env var
145
148
  },
149
+ robinhood: {
150
+ version: "1.2",
151
+ chainId: 4663,
152
+ chain: robinhood,
153
+ defaultRpc: "https://rpc.mainnet.chain.robinhood.com",
154
+ splitter: "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB",
155
+ // no USDC on Robinhood Chain — native ETH only
156
+ explorer: "https://robinhoodchain.blockscout.com",
157
+ nativeUsdEnv: "AIFINPAY_ETH_USD",
158
+ nativeUsdDefault: 1870, // reference only; ~$1870 on 2026-08-03
159
+ },
146
160
  xrplevm: {
147
161
  version: "1.2",
148
162
  chainId: 1440000,
@@ -163,10 +177,11 @@ const EVM_CHAIN_OBJECTS = {
163
177
  arbitrum,
164
178
  optimism,
165
179
  base,
180
+ robinhood,
166
181
  };
167
182
  function evmChainObject(name) {
168
- return (EVM_CHAIN_OBJECTS[name]
169
- ?? SPLITTER_DEPLOYMENTS[name]?.chain);
183
+ return (EVM_CHAIN_OBJECTS[name] ??
184
+ SPLITTER_DEPLOYMENTS[name]?.chain);
170
185
  }
171
186
  // Mainnet USDC SPL mint on Solana (Circle native, not Wormhole-wrapped USDCet)
172
187
  const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
@@ -295,8 +310,7 @@ export class AiFinPayAgent {
295
310
  constructor(inner, evmAccount, opts = {}) {
296
311
  this.inner = inner;
297
312
  this.evmAccount = evmAccount;
298
- this.registryUrl = opts.registryUrl
299
- ?? `${inner.baseUrl}${DEFAULT_REGISTRY_PATH}`;
313
+ this.registryUrl = opts.registryUrl ?? `${inner.baseUrl}${DEFAULT_REGISTRY_PATH}`;
300
314
  this.registryCandidates = opts.registryUrl
301
315
  ? [opts.registryUrl]
302
316
  : DEFAULT_REGISTRY_PATHS.map((path) => `${inner.baseUrl}${path}`);
@@ -311,9 +325,7 @@ export class AiFinPayAgent {
311
325
  this.ledgerOverride = opts.spendLedger;
312
326
  this.telemetry = opts.telemetry !== false;
313
327
  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";
328
+ this.solanaRpc = opts.solanaRpc ?? process.env.AIFINPAY_SOLANA_RPC ?? "https://api.mainnet-beta.solana.com";
317
329
  // Optional RPC overrides for non-Polygon EVM chains used in bridge flows.
318
330
  // Falls back to viem's chain.rpcUrls.default if not provided.
319
331
  if (opts.evmRpcUrls)
@@ -354,8 +366,7 @@ export class AiFinPayAgent {
354
366
  if (!chain) {
355
367
  throw new AiFinPayError(`evmClients: unsupported EVM chain "${name}"`);
356
368
  }
357
- const rpcUrl = this.evmRpcUrls[name]
358
- ?? SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
369
+ const rpcUrl = this.evmRpcUrls[name] ?? SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
359
370
  const transport = rpcUrl ? http(rpcUrl) : http();
360
371
  const publicClient = createPublicClient({ chain, transport });
361
372
  const walletClient = createWalletClient({ chain, transport, account: this.evmAccount });
@@ -373,8 +384,30 @@ export class AiFinPayAgent {
373
384
  }
374
385
  // ── Constructors ─────────────────────────────────────────────────────────
375
386
  /**
376
- * Generate a fresh agent — new Solana keypair + new EVM keypair.
377
- * Use `fromSeed()` to derive both from a single backup phrase instead.
387
+ * Load an existing local identity without creating or replacing a wallet.
388
+ * Priority: SEED_HASH → ./aifinpay/agents.json → AIFINPAY_AGENT_SECRET →
389
+ * the MCP keystore at AIFINPAY_HOME/agent.json (default ~/.aifinpay).
390
+ * AIFINPAY_AGENTS_FILE/AGENT_ID select a project file/record; encrypted
391
+ * keystores require AIFINPAY_WALLET_PASSPHRASE. Reads process.env, not .env.
392
+ * Missing, invalid or ambiguous configuration throws before any network call.
393
+ */
394
+ static async fromEnvironment(opts = {}) {
395
+ const identity = loadLocalWalletIdentity();
396
+ try {
397
+ return identity.seedHash !== undefined
398
+ ? await AiFinPayAgent.fromSeed(identity.seedHash, opts)
399
+ : await AiFinPayAgent.fromSolanaSecret(identity.secretB58, opts);
400
+ }
401
+ catch {
402
+ // Decoder/library errors can include input. Never echo local key material.
403
+ throw new AiFinPayError(`Invalid wallet selected from ${identity.source}; refusing to generate a replacement wallet`);
404
+ }
405
+ }
406
+ /**
407
+ * Generate a fresh, ephemeral agent. Every call selects a new identity;
408
+ * existing environment variables and keystores are NOT loaded or changed.
409
+ * This method does not persist its keys. Use fromEnvironment() for a funded
410
+ * existing wallet, or privately persist the new seed/secret before funding.
378
411
  */
379
412
  static async new(opts = {}) {
380
413
  // Seed-derived, NOT an independent random EVM key.
@@ -411,17 +444,20 @@ export class AiFinPayAgent {
411
444
  * TODO(phase-1): replace with BIP-39/BIP-44 derivation
412
445
  * m/44'/501'/0'/0' (Solana) + m/44'/60'/0'/0/0 (EVM) once the
413
446
  * wallet-import audit recommendation lands.
447
+ * An explicit opts.evmPrivateKey imports an existing EVM wallet instead;
448
+ * keep that key as well as the seed to restore the same EVM address.
414
449
  */
415
450
  static async fromSeed(seedHex, opts = {}) {
416
- const seed = hexToBytes(seedHex.replace(/^0x/, ""));
417
- if (seed.length !== 32) {
451
+ const normalizedSeed = seedHex.replace(/^0x/, "");
452
+ if (!/^[0-9a-fA-F]{64}$/.test(normalizedSeed)) {
418
453
  throw new AiFinPayError(`fromSeed: seed must be 32 bytes (64 hex chars)`);
419
454
  }
455
+ const seed = hexToBytes(normalizedSeed);
420
456
  const kp = nacl.sign.keyPair.fromSeed(seed);
421
- const inner = Agent
422
- ._ofKeyPair?.(kp, opts) ?? Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
457
+ const inner = Agent._ofKeyPair?.(kp, opts) ??
458
+ Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
423
459
  // Derive EVM key from seed (independent, not BIP-44 — see TODO above)
424
- const evmHex = ("0x" + bytesToHex(crypto32(seed)));
460
+ const evmHex = opts.evmPrivateKey ?? ("0x" + bytesToHex(crypto32(seed)));
425
461
  const evmAccount = privateKeyToAccount(evmHex);
426
462
  return new AiFinPayAgent(inner, evmAccount, opts);
427
463
  }
@@ -439,8 +475,7 @@ export class AiFinPayAgent {
439
475
  */
440
476
  static async fromSolanaSecret(secretB58, opts = {}) {
441
477
  const inner = Agent.fromSecretB58(secretB58, opts);
442
- const evmKey = opts.evmPrivateKey
443
- ?? ("0x" + bytesToHex(crypto32(inner.secretKey.subarray(0, 32))));
478
+ const evmKey = opts.evmPrivateKey ?? ("0x" + bytesToHex(crypto32(inner.secretKey.subarray(0, 32))));
444
479
  const evmAccount = privateKeyToAccount(evmKey);
445
480
  return new AiFinPayAgent(inner, evmAccount, opts);
446
481
  }
@@ -449,8 +484,12 @@ export class AiFinPayAgent {
449
484
  // Canonical: Solana pubkey for back-compat with the leaderboard / Seat PDA.
450
485
  return this.inner.address;
451
486
  }
452
- get solanaAddress() { return this.inner.address; }
453
- get evmAddress() { return this.evmAccount.address; }
487
+ get solanaAddress() {
488
+ return this.inner.address;
489
+ }
490
+ get evmAddress() {
491
+ return this.evmAccount.address;
492
+ }
454
493
  /**
455
494
  * Casper identity, derived from the same seed as the other two.
456
495
  *
@@ -466,7 +505,9 @@ export class AiFinPayAgent {
466
505
  return casperIdentityFromSeed(this.inner.secretKey.subarray(0, 32));
467
506
  }
468
507
  /** Convenience: the account hash, which is what a Casper explorer wants. */
469
- get casperAddress() { return this.casper.accountHash; }
508
+ get casperAddress() {
509
+ return this.casper.accountHash;
510
+ }
470
511
  // ── Registry ────────────────────────────────────────────────────────────
471
512
  async fetchRegistry(force = false) {
472
513
  if (this.cachedRegistry && !force)
@@ -541,9 +582,7 @@ export class AiFinPayAgent {
541
582
  description: opts.description ?? null,
542
583
  endpoint: opts.endpoint,
543
584
  capabilities: opts.capabilities ?? [],
544
- pricing: opts.pricing
545
- ? { per_call: opts.pricing.perCall, currency: opts.pricing.currency ?? "USDC" }
546
- : null,
585
+ pricing: opts.pricing ? { per_call: opts.pricing.perCall, currency: opts.pricing.currency ?? "USDC" } : null,
547
586
  nonce,
548
587
  signature,
549
588
  }),
@@ -629,9 +668,8 @@ export class AiFinPayAgent {
629
668
  if (this.ledgerOverride)
630
669
  return this.ledgerOverride;
631
670
  if (!this._ledger) {
632
- this._ledger = this.budgetCaps.daily_usd !== undefined
633
- ? FileSpendLedger.forAgent(this.evmAddress)
634
- : new MemorySpendLedger();
671
+ this._ledger =
672
+ this.budgetCaps.daily_usd !== undefined ? FileSpendLedger.forAgent(this.evmAddress) : new MemorySpendLedger();
635
673
  }
636
674
  return this._ledger;
637
675
  }
@@ -671,23 +709,6 @@ export class AiFinPayAgent {
671
709
  return false;
672
710
  throw err;
673
711
  }
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
712
  /**
692
713
  * Current 24-hour rolling spend across all paid calls made by this
693
714
  * agent instance. Useful for budget dashboards on the consumer side.
@@ -695,6 +716,16 @@ export class AiFinPayAgent {
695
716
  getSpend24h() {
696
717
  return this.spend24h.total24h();
697
718
  }
719
+ /**
720
+ * Durable daily spend from the budget ledger (settled + outstanding, 24h
721
+ * window). Unlike getSpend24h()'s in-memory ring, this survives reload and
722
+ * reflects reserve/commit accounting even with no signing tool attached —
723
+ * the number a long-lived host (e.g. MCP) should display. 0 when no
724
+ * daily_usd cap is set and nothing was reserved.
725
+ */
726
+ async getDailySpendUsd() {
727
+ return this.ledger.total(24 * 3600 * 1000);
728
+ }
698
729
  /**
699
730
  * Sanity-check the USD estimate of an on-chain challenge amount against
700
731
  * the declared cost and the per-call cap BEFORE signing the tx.
@@ -725,13 +756,21 @@ export class AiFinPayAgent {
725
756
  */
726
757
  priceCache;
727
758
  async nativeUsdFor(deployment) {
728
- const env = process.env[deployment.nativeUsdEnv];
759
+ return this.tokenUsd(deployment.chain.nativeCurrency.symbol.toUpperCase(), deployment.nativeUsdEnv);
760
+ }
761
+ /**
762
+ * Native-token USD price: explicit env var, then the protocol price feed,
763
+ * then NaN ("unknown") — never a stale constant. Callers decide what an
764
+ * unknown price means for their flow (balance() excludes the leg;
765
+ * guardChallengeAmount declines to block).
766
+ */
767
+ async tokenUsd(symbol, envVar) {
768
+ const env = process.env[envVar];
729
769
  if (env !== undefined) {
730
770
  const v = parseFloat(env);
731
771
  if (Number.isFinite(v) && v > 0)
732
- return { usd: v, source: deployment.nativeUsdEnv };
772
+ return { usd: v, source: envVar };
733
773
  }
734
- const symbol = deployment.chain.nativeCurrency.symbol.toUpperCase();
735
774
  const fresh = this.priceCache && Date.now() - this.priceCache.at < 60_000;
736
775
  if (!fresh) {
737
776
  try {
@@ -771,10 +810,7 @@ export class AiFinPayAgent {
771
810
  let balance;
772
811
  let gasPrice;
773
812
  try {
774
- [balance, gasPrice] = await Promise.all([
775
- publicClient.getBalance({ address }),
776
- publicClient.getGasPrice(),
777
- ]);
813
+ [balance, gasPrice] = await Promise.all([publicClient.getBalance({ address }), publicClient.getGasPrice()]);
778
814
  }
779
815
  catch {
780
816
  // An RPC that will not answer must not turn into a funding error; let
@@ -795,7 +831,7 @@ export class AiFinPayAgent {
795
831
  return s.includes(".") ? s.replace(/(\.\d{6})\d+$/, "$1").replace(/\.?0+$/, "") || "0" : s;
796
832
  };
797
833
  const { usd } = await this.nativeUsdFor(deployment);
798
- const toUsd = (wei) => Number.isFinite(usd) ? (Number(wei) / 1e18) * usd : Number.NaN;
834
+ const toUsd = (wei) => (Number.isFinite(usd) ? (Number(wei) / 1e18) * usd : Number.NaN);
799
835
  throw new InsufficientFundsError(toUsd(needed), toUsd(balance), `Agent ${address} holds ${short(balance)} ${symbol} on ${chainName}, but this call needs ` +
800
836
  `about ${short(needed)} ${symbol} — ${short(valueWei)} for the payment and up to ` +
801
837
  `${short(gasBudget)} for gas. The gas figure is a ceiling: it assumes 200k gas at the ` +
@@ -909,8 +945,8 @@ export class AiFinPayAgent {
909
945
  async bridgeQuote(opts) {
910
946
  const fromToken = opts.fromToken ?? USDC_NATIVE[opts.fromChain];
911
947
  const toToken = opts.toToken ?? USDC_NATIVE[opts.toChain];
912
- const fromAmount = opts.fromAmount
913
- ?? (opts.amount_usdc !== undefined
948
+ const fromAmount = opts.fromAmount ??
949
+ (opts.amount_usdc !== undefined
914
950
  ? Math.round(opts.amount_usdc * 1e6).toString() // USDC has 6 decimals
915
951
  : undefined);
916
952
  if (!fromAmount) {
@@ -948,14 +984,10 @@ export class AiFinPayAgent {
948
984
  }
949
985
  // ── Call ────────────────────────────────────────────────────────────────
950
986
  /**
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).
987
+ * Legacy bridge call. Free/read-only bridge responses are passed through;
988
+ * paid 402 challenges fail closed because this legacy settlement surface has
989
+ * no independently reviewed target. Use the reviewed fetchPaid v1.3 route
990
+ * with a fresh trusted quote/FX result for paid requests.
959
991
  *
960
992
  * Returns `null` (instead of a `Response`) iff a budget cap was hit
961
993
  * AND budget.on_limit_exceeded is set to "skip" — the call is dropped
@@ -975,7 +1007,7 @@ export class AiFinPayAgent {
975
1007
  const reservation = await this.reserveDaily(cost);
976
1008
  if (reservation === "skip")
977
1009
  return null;
978
- let settled = false;
1010
+ const settled = false;
979
1011
  try {
980
1012
  if (provider.mode === "session") {
981
1013
  throw new AiFinPayError(`Provider ${provider.name} requires session mode — use openSession() (phase-3 feature)`);
@@ -1000,8 +1032,7 @@ export class AiFinPayAgent {
1000
1032
  headers: { "content-type": "application/json", ...extraHeaders },
1001
1033
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
1002
1034
  // CallOptions.timeoutMs was previously declared but never applied.
1003
- signal: opts.signal
1004
- ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined),
1035
+ signal: opts.signal ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined),
1005
1036
  });
1006
1037
  // 1. Initial unauthenticated POST → expect 402 challenge from the bridge.
1007
1038
  const fullUrl = url.replace(/\/$/, "") + path;
@@ -1020,119 +1051,22 @@ export class AiFinPayAgent {
1020
1051
  catch (e) {
1021
1052
  throw new X402Error(`bridge returned 402 with non-JSON body`);
1022
1053
  }
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(", ")})`);
1075
- }
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(", ")}`);
1079
- }
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`);
1054
+ // Legacy bridge settlement is fail-closed. The challenge historically
1055
+ // supplied arbitrary splitter/program addresses and native amounts, so
1056
+ // continuing from here could sign against an unreviewed target or let a
1057
+ // missing price bypass the budget. Keep free/read-only calls above, but
1058
+ // refuse every paid legacy challenge before any signing or broadcast.
1059
+ if (!(typeof provider.price_usd === "number" && Number.isFinite(provider.price_usd) && provider.price_usd > 0) ||
1060
+ !(typeof cost === "number" && Number.isFinite(cost) && cost > 0)) {
1061
+ throw new AiFinPayError(`legacy call() refused ${provider.name}: the provider has no trusted positive USD price; ` +
1062
+ `use the reviewed fetchPaid v1.3 route with fresh trusted FX instead`);
1085
1063
  }
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;
1064
+ if (!legacySettlementIsReviewed()) {
1065
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1066
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1098
1067
  }
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;
1068
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1069
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1136
1070
  }
1137
1071
  finally {
1138
1072
  // Every path that did not move money gives the budget back now rather
@@ -1156,87 +1090,14 @@ export class AiFinPayAgent {
1156
1090
  // a reverted transaction would otherwise be handed to a merchant as proof of
1157
1091
  // a payment that never moved.
1158
1092
  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;
1093
+ void p;
1094
+ throw new AiFinPayError("legacy splitter settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1095
+ }
1096
+ // Legacy Solana settlement remains a named private surface only so older
1097
+ // callers fail with a clear migration error instead of reaching a signer.
1098
+ async submitSolanaB2BPayWithSplit(ps) {
1099
+ void ps;
1100
+ throw new AiFinPayError("legacy Solana settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1240
1101
  }
1241
1102
  // ── AIFP-1 merchant paywall (gateway.aifinpay.io) ───────────────────────
1242
1103
  //
@@ -1252,16 +1113,43 @@ export class AiFinPayAgent {
1252
1113
  * test asserting that a receipt was reused, both need to see it. The entries
1253
1114
  * contain bearer JWTs — treat them like keys, and do not log them.
1254
1115
  */
1255
- get aifp1Receipts() { return this._aifp1Cache; }
1116
+ get aifp1Receipts() {
1117
+ return this._aifp1Cache;
1118
+ }
1256
1119
  /**
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.
1120
+ * Redacted receipt-cache view (no bearer JWTs): what batches this instance
1121
+ * holds, for shutdown decisions and dashboards. Safe to log and to expose
1122
+ * over MCP — use this, never `aifp1Receipts.list()`.
1262
1123
  */
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");
1124
+ getReceiptCacheSummary() {
1125
+ return this._aifp1Cache.summary();
1126
+ }
1127
+ /**
1128
+ * Use the canonical invoice executor only with a caller-supplied deployment
1129
+ * pin. An unauthenticated quote cannot establish its own signing authority.
1130
+ */
1131
+ async settleAifp1NativeV13(p, opts) {
1132
+ const pin = opts.settlementPin;
1133
+ if (!pin || pin.route_class !== "AIFP-1" || pin.chain !== "polygon" || pin.testnet) {
1134
+ throw new AiFinPayError("AIFP-1 fetchPaid requires an independent trusted Polygon v1.3 settlement pin; other receipt networks are not supported");
1135
+ }
1136
+ const client = new SettlementClient({
1137
+ baseUrl: opts.apiBaseUrl ?? "https://api.aifinpay.io",
1138
+ fetchImpl: this.inner.fetchImpl,
1139
+ timeoutMs: this.inner.timeoutMs,
1140
+ });
1141
+ const invoice = await client.invoice({
1142
+ route_class: "AIFP-1",
1143
+ chain: "polygon",
1144
+ asset: "POL",
1145
+ gross_amount: p.grossWei,
1146
+ merchant_wallet: p.merchantWallet,
1147
+ order_id: p.orderId,
1148
+ valid_until: Number(p.validUntil),
1149
+ });
1150
+ const { publicClient, walletClient } = this.polygonClients();
1151
+ const execution = await executeSettlementInvoice(invoice, walletClient, publicClient, pin);
1152
+ return execution.settlement_tx;
1265
1153
  }
1266
1154
  /**
1267
1155
  * Fetch a paywalled URL, paying the AIFP-1 gateway if it asks.
@@ -1285,7 +1173,9 @@ export class AiFinPayAgent {
1285
1173
  // (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
1286
1174
  // would silently opt the agent out of its owner's own limits.
1287
1175
  agentId: opts.agentId ?? this.evmAddress,
1288
- settle: (p) => this.settleAifp1NativeV13(p),
1176
+ payerAddress: this.evmAddress,
1177
+ signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
1178
+ settle: (p) => this.settleAifp1NativeV13(p, opts),
1289
1179
  checkPerCall: (usd) => this.checkPerCall(usd),
1290
1180
  reserveDaily: (usd) => this.reserveDaily(usd),
1291
1181
  commit: (id, usd) => this.ledger.commit(id, usd),
@@ -1294,77 +1184,57 @@ export class AiFinPayAgent {
1294
1184
  this.spend24h.add(amountUsd);
1295
1185
  if (this.telemetry) {
1296
1186
  this.reportTelemetry({
1297
- kind: "aifp1", merchant: merchantId, chain: "polygon",
1298
- cost: amountUsd, tx: txRef,
1187
+ kind: "aifp1",
1188
+ merchant: merchantId,
1189
+ chain: "polygon",
1190
+ cost: amountUsd,
1191
+ tx: txRef,
1299
1192
  });
1300
1193
  }
1301
1194
  },
1302
1195
  };
1303
1196
  return aifp1Fetch(deps, url, init, opts);
1304
1197
  }
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,
1198
+ /**
1199
+ * List verified settlement routes. Non-signing: fetches + returns the
1200
+ * backend route table (same client the MCP `settlement_routes` tool should
1201
+ * use instead of its own inline HTTP). Never signs or broadcasts.
1202
+ */
1203
+ async settlementRoutes(routeClass, baseUrl) {
1204
+ return this.settlementClient(baseUrl).routes(routeClass);
1205
+ }
1206
+ /**
1207
+ * Request a settlement invoice and validate it (canonical split,
1208
+ * chain binding, request match). Non-signing: returns the invoice for
1209
+ * display/approval. Never signs or broadcasts.
1210
+ */
1211
+ async requestSettlementInvoice(input, baseUrl) {
1212
+ return this.settlementClient(baseUrl).invoice(input);
1213
+ }
1214
+ settlementClient(baseUrl) {
1215
+ return new SettlementClient({
1216
+ baseUrl: baseUrl ?? "https://api.aifinpay.io",
1217
+ fetchImpl: this.inner.fetchImpl,
1218
+ timeoutMs: this.inner.timeoutMs,
1356
1219
  });
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",
1220
+ }
1221
+ /**
1222
+ * Prepaid quota batches for this agent's address, most room first with a
1223
+ * per-merchant rollup. Non-signing read of retained receipts metadata.
1224
+ */
1225
+ async getQuota(opts = {}) {
1226
+ return getQuota({
1227
+ address: this.evmAddress,
1228
+ merchantId: opts.merchantId,
1229
+ includeExhausted: opts.includeExhausted,
1230
+ baseUrl: opts.baseUrl,
1231
+ fetchImpl: this.inner.fetchImpl,
1362
1232
  });
1363
- return sig;
1364
1233
  }
1365
1234
  // ── Sessions ────────────────────────────────────────────────────────────
1366
1235
  /**
1367
1236
  * TODO(phase-3): open a metered budget session.
1237
+ * @deprecated Not implemented — always throws. Tracked for phase 3; do not call.
1368
1238
  */
1369
1239
  async openSession(_args) {
1370
1240
  throw new AiFinPayError("openSession() not implemented yet — see migration phase 3");
@@ -1373,24 +1243,32 @@ export class AiFinPayAgent {
1373
1243
  /**
1374
1244
  * Snapshot the agent's funds across both chains, USD-normalised.
1375
1245
  *
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.
1246
+ * Native prices are env-first (`AIFINPAY_MATIC_USD` / `AIFINPAY_SOL_USD`),
1247
+ * then the protocol price feed — never a stale compiled-in constant. A leg
1248
+ * with no known price is excluded from the total and listed in
1249
+ * `unknown_legs` instead of being fabricated.
1380
1250
  */
1381
1251
  async balance() {
1382
- const maticUsd = parseFloat(process.env.AIFINPAY_MATIC_USD ?? "0.70");
1383
- const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
1252
+ const [pol, sol] = await Promise.all([
1253
+ this.tokenUsd("POL", SPLITTER_DEPLOYMENTS.polygon.nativeUsdEnv),
1254
+ this.tokenUsd("SOL", "AIFINPAY_SOL_USD"),
1255
+ ]);
1384
1256
  const polygon = await this.fetchPolygonNative().catch(() => 0);
1385
1257
  const solana = await this.fetchSolanaNative().catch(() => 0);
1386
1258
  const solana_usdc = await this.fetchSolanaUsdc().catch(() => 0);
1387
1259
  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
1260
+ const unknown_legs = [];
1261
+ let agent_balance_usd = solana_usdc + polygon_usdc; // USDC ≈ $1
1262
+ if (Number.isFinite(pol.usd))
1263
+ agent_balance_usd += polygon * pol.usd;
1264
+ else
1265
+ unknown_legs.push("pol");
1266
+ if (Number.isFinite(sol.usd))
1267
+ agent_balance_usd += solana * sol.usd;
1268
+ else
1269
+ unknown_legs.push("sol");
1392
1270
  return {
1393
- agent_balance_usd: polygonUsd + solanaUsd + solanaUsdcUsd + polygonUsdcUsd,
1271
+ agent_balance_usd,
1394
1272
  chains: {
1395
1273
  solana: { sol: solana, usdc: solana_usdc, msecco_balance: 0 },
1396
1274
  polygon: { matic: polygon, usdc: polygon_usdc, msecco_balance: 0 },
@@ -1398,6 +1276,8 @@ export class AiFinPayAgent {
1398
1276
  spend_24h_usd: this.spend24h.total24h(),
1399
1277
  budget_caps: this.budgetCaps,
1400
1278
  priced_at: Math.floor(Date.now() / 1000),
1279
+ prices: { pol, sol },
1280
+ ...(unknown_legs.length ? { unknown_legs } : {}),
1401
1281
  };
1402
1282
  }
1403
1283
  async fetchPolygonNative() {
@@ -1444,11 +1324,7 @@ export class AiFinPayAgent {
1444
1324
  jsonrpc: "2.0",
1445
1325
  id: 1,
1446
1326
  method: "getTokenAccountsByOwner",
1447
- params: [
1448
- this.solanaAddress,
1449
- { mint: USDC_SOLANA_MINT },
1450
- { encoding: "jsonParsed" },
1451
- ],
1327
+ params: [this.solanaAddress, { mint: USDC_SOLANA_MINT }, { encoding: "jsonParsed" }],
1452
1328
  }),
1453
1329
  });
1454
1330
  if (!r.ok)
@@ -1486,8 +1362,8 @@ export class AiFinPayAgent {
1486
1362
  * six registry chains against mainnet.
1487
1363
  *
1488
1364
  * 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.
1365
+ * only for this read-only diagnostic when code cannot be read. The legacy
1366
+ * payment path is disabled regardless of the reported or detected version.
1491
1367
  */
1492
1368
  splitterVersionCache = new Map();
1493
1369
  async detectSplitterVersion(splitter, chainName, fallback) {
@@ -1533,6 +1409,7 @@ export class AiFinPayAgent {
1533
1409
  // ── Reputation ──────────────────────────────────────────────────────────
1534
1410
  /**
1535
1411
  * TODO(phase-4 / phase-5): query operator backend reputation engine.
1412
+ * @deprecated Zero-stub — returns unverified zeros until the engine lands; do not treat as signal.
1536
1413
  */
1537
1414
  async reputation() {
1538
1415
  return {
@@ -1546,13 +1423,19 @@ export class AiFinPayAgent {
1546
1423
  // ── Telemetry (opt-out) ─────────────────────────────────────────────────
1547
1424
  reportTelemetry(payload) {
1548
1425
  // Fire-and-forget. No body content, only metadata.
1549
- void this.inner.fetchImpl(`${this.inner.baseUrl}/api/telemetry`, {
1426
+ void this.inner
1427
+ .fetchImpl(`${this.inner.baseUrl}/api/telemetry`, {
1550
1428
  method: "POST",
1551
1429
  headers: { "content-type": "application/json" },
1552
1430
  body: JSON.stringify({ agent_id: this.id, ...payload, ts: Date.now() }),
1553
- }).catch(() => { });
1431
+ })
1432
+ .catch(() => { });
1554
1433
  }
1555
1434
  }
1435
+ /** Legacy bridge settlement has no reviewed mainnet route; keep this closed. */
1436
+ function legacySettlementIsReviewed() {
1437
+ return false;
1438
+ }
1556
1439
  // ── Internal hex helpers (small, no extra deps) ────────────────────────────
1557
1440
  function hexToBytes(hex) {
1558
1441
  const out = new Uint8Array(hex.length / 2);