@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.
- 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 +94 -5
- package/dist/aifp1.d.ts.map +1 -1
- package/dist/aifp1.js +370 -114
- 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 +1 -0
- package/dist/crossChain.d.ts.map +1 -1
- package/dist/crossChain.js +87 -5
- 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 +35 -5
- package/dist/settlement.d.ts.map +1 -1
- package/dist/settlement.js +232 -76
- 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 +97 -29
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +232 -349
- 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
|
-
import { botchain, xrplevm } from "./chains.js";
|
|
20
|
-
import {
|
|
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
|
-
|
|
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
|
-
*
|
|
377
|
-
*
|
|
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
|
|
417
|
-
if (
|
|
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
|
-
|
|
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() {
|
|
453
|
-
|
|
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() {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
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).
|
|
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
|
-
|
|
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
|
-
//
|
|
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(", ")})`);
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
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;
|
|
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() {
|
|
1116
|
+
get aifp1Receipts() {
|
|
1117
|
+
return this._aifp1Cache;
|
|
1118
|
+
}
|
|
1256
1119
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
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
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
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",
|
|
1298
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
* `
|
|
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
|
|
1383
|
-
|
|
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
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
|
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
|
|
1490
|
-
*
|
|
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
|
|
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
|
-
})
|
|
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);
|