@aifinpay/agent 2.0.0-rc.7 → 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.
- package/LICENSE +21 -0
- package/PAYMENT_RECEIPTS.md +114 -0
- package/README.md +85 -28
- package/dist/agent.d.ts +8 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +50 -17
- package/dist/agent.js.map +1 -1
- package/dist/agentHistory.d.ts +55 -0
- package/dist/agentHistory.d.ts.map +1 -0
- package/dist/agentHistory.js +145 -0
- package/dist/agentHistory.js.map +1 -0
- package/dist/agentPassport.d.ts +71 -7
- package/dist/agentPassport.d.ts.map +1 -1
- package/dist/agentPassport.js +135 -27
- package/dist/agentPassport.js.map +1 -1
- package/dist/aifp1.d.ts +126 -5
- package/dist/aifp1.d.ts.map +1 -1
- package/dist/aifp1.js +407 -108
- package/dist/aifp1.js.map +1 -1
- package/dist/apiUrl.d.ts +3 -0
- package/dist/apiUrl.d.ts.map +1 -0
- package/dist/apiUrl.js +8 -0
- package/dist/apiUrl.js.map +1 -0
- package/dist/chains.d.ts +7 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +19 -1
- package/dist/chains.js.map +1 -1
- package/dist/crossChain.d.ts.map +1 -1
- package/dist/crossChain.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js.map +1 -1
- package/dist/deploymentResolver.d.ts +95 -0
- package/dist/deploymentResolver.d.ts.map +1 -0
- package/dist/deploymentResolver.js +169 -0
- package/dist/deploymentResolver.js.map +1 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -1
- package/dist/facilitators/aifinpay.d.ts +4 -5
- package/dist/facilitators/aifinpay.d.ts.map +1 -1
- package/dist/facilitators/aifinpay.js +50 -21
- package/dist/facilitators/aifinpay.js.map +1 -1
- package/dist/facilitators/base.d.ts +12 -1
- package/dist/facilitators/base.d.ts.map +1 -1
- package/dist/facilitators/coinbase.d.ts.map +1 -1
- package/dist/facilitators/coinbase.js +3 -7
- package/dist/facilitators/coinbase.js.map +1 -1
- package/dist/facilitators/detect.d.ts.map +1 -1
- package/dist/facilitators/detect.js +2 -7
- package/dist/facilitators/detect.js.map +1 -1
- package/dist/facilitators/standard-x402.d.ts.map +1 -1
- package/dist/facilitators/standard-x402.js +40 -30
- package/dist/facilitators/standard-x402.js.map +1 -1
- package/dist/generated/solanaV14Deployments.generated.d.ts +25 -0
- package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -0
- package/dist/generated/solanaV14Deployments.generated.js +39 -0
- package/dist/generated/solanaV14Deployments.generated.js.map +1 -0
- package/dist/generated/splitterRoutes.generated.d.ts +24 -0
- package/dist/generated/splitterRoutes.generated.d.ts.map +1 -0
- package/dist/generated/splitterRoutes.generated.js +453 -0
- package/dist/generated/splitterRoutes.generated.js.map +1 -0
- package/dist/generated/v14Deployments.generated.d.ts +49 -0
- package/dist/generated/v14Deployments.generated.d.ts.map +1 -0
- package/dist/generated/v14Deployments.generated.js +415 -0
- package/dist/generated/v14Deployments.generated.js.map +1 -0
- package/dist/index.d.ts +27 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/localIdentity.d.ts +17 -0
- package/dist/localIdentity.d.ts.map +1 -0
- package/dist/localIdentity.js +95 -0
- package/dist/localIdentity.js.map +1 -0
- package/dist/settlement.d.ts +34 -4
- package/dist/settlement.d.ts.map +1 -1
- package/dist/settlement.js +233 -75
- package/dist/settlement.js.map +1 -1
- package/dist/settlementHttp.d.ts +10 -0
- package/dist/settlementHttp.d.ts.map +1 -0
- package/dist/settlementHttp.js +78 -0
- package/dist/settlementHttp.js.map +1 -0
- package/dist/settlementV14.d.ts +10 -9
- package/dist/settlementV14.d.ts.map +1 -1
- package/dist/settlementV14.js +43 -55
- package/dist/settlementV14.js.map +1 -1
- package/dist/solanaDeploymentResolver.d.ts +98 -0
- package/dist/solanaDeploymentResolver.d.ts.map +1 -0
- package/dist/solanaDeploymentResolver.js +162 -0
- package/dist/solanaDeploymentResolver.js.map +1 -0
- package/dist/solanaV14Deployments.generated.d.ts +25 -0
- package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
- package/dist/solanaV14Deployments.generated.js +39 -0
- package/dist/solanaV14Deployments.generated.js.map +1 -0
- package/dist/spendLedger.d.ts.map +1 -1
- package/dist/spendLedger.js +1 -3
- package/dist/spendLedger.js.map +1 -1
- package/dist/splitterRoutes.d.ts +11 -4
- package/dist/splitterRoutes.d.ts.map +1 -1
- package/dist/splitterRoutes.generated.d.ts +11 -9
- package/dist/splitterRoutes.generated.d.ts.map +1 -1
- package/dist/splitterRoutes.generated.js +72 -18
- package/dist/splitterRoutes.generated.js.map +1 -1
- package/dist/splitterRoutes.js +7 -5
- package/dist/splitterRoutes.js.map +1 -1
- package/dist/unifiedAgent.d.ts +96 -28
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +219 -348
- package/dist/unifiedAgent.js.map +1 -1
- package/dist/v14Deployments.generated.d.ts +49 -0
- package/dist/v14Deployments.generated.d.ts.map +1 -0
- package/dist/v14Deployments.generated.js +413 -0
- package/dist/v14Deployments.generated.js.map +1 -0
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +2 -2
- package/dist/wallet.js.map +1 -1
- package/package.json +27 -6
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -127
- package/dist/cli.js.map +0 -1
package/dist/unifiedAgent.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
377
|
-
*
|
|
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
|
|
417
|
-
if (
|
|
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
|
-
|
|
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() {
|
|
453
|
-
|
|
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() {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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() {
|
|
1104
|
+
get aifp1Receipts() {
|
|
1105
|
+
return this._aifp1Cache;
|
|
1106
|
+
}
|
|
1256
1107
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
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
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
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",
|
|
1298
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
* `
|
|
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
|
|
1383
|
-
|
|
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
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
|
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
|
|
1490
|
-
*
|
|
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
|
|
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
|
-
})
|
|
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);
|