@aifinpay/agent 0.3.0-alpha.5 → 1.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/README.md +6 -6
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -2
- package/dist/agent.js.map +1 -1
- package/dist/facilitators/standard-x402.d.ts +9 -0
- package/dist/facilitators/standard-x402.d.ts.map +1 -0
- package/dist/facilitators/standard-x402.js +133 -0
- package/dist/facilitators/standard-x402.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/unifiedAgent.d.ts +177 -5
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +413 -27
- package/dist/unifiedAgent.js.map +1 -1
- package/package.json +2 -2
package/dist/unifiedAgent.js
CHANGED
|
@@ -42,6 +42,31 @@ const EVM_CHAIN_OBJECTS = {
|
|
|
42
42
|
optimism,
|
|
43
43
|
base,
|
|
44
44
|
};
|
|
45
|
+
// Mainnet USDC SPL mint on Solana (Circle native, not Wormhole-wrapped USDCet)
|
|
46
|
+
const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
47
|
+
// Native Polygon USDC ERC20 (Circle-native, not bridged USDC.e)
|
|
48
|
+
const USDC_POLYGON_ERC20 = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
|
|
49
|
+
// Minimal ERC20 balanceOf ABI fragment
|
|
50
|
+
const ERC20_BALANCE_OF_ABI = [
|
|
51
|
+
{
|
|
52
|
+
type: "function",
|
|
53
|
+
name: "balanceOf",
|
|
54
|
+
stateMutability: "view",
|
|
55
|
+
inputs: [{ type: "address", name: "owner" }],
|
|
56
|
+
outputs: [{ type: "uint256" }],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
// B2BSplitter.treasury() view — used to route the ipCreator royalty slot
|
|
60
|
+
// when the bridge challenge doesn't name a recipient.
|
|
61
|
+
const SPLITTER_TREASURY_ABI = [
|
|
62
|
+
{
|
|
63
|
+
type: "function",
|
|
64
|
+
name: "treasury",
|
|
65
|
+
stateMutability: "view",
|
|
66
|
+
inputs: [],
|
|
67
|
+
outputs: [{ type: "address" }],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
45
70
|
// ── Errors ─────────────────────────────────────────────────────────────────
|
|
46
71
|
export class ProviderUnknownError extends AiFinPayError {
|
|
47
72
|
}
|
|
@@ -131,9 +156,8 @@ export class AiFinPayAgent {
|
|
|
131
156
|
?? "https://api.mainnet-beta.solana.com";
|
|
132
157
|
// Optional RPC overrides for non-Polygon EVM chains used in bridge flows.
|
|
133
158
|
// Falls back to viem's chain.rpcUrls.default if not provided.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.evmRpcUrls = evmOpts.evmRpcUrls;
|
|
159
|
+
if (opts.evmRpcUrls)
|
|
160
|
+
this.evmRpcUrls = opts.evmRpcUrls;
|
|
137
161
|
if (this.polygonRpc)
|
|
138
162
|
this.evmRpcUrls.polygon = this.polygonRpc;
|
|
139
163
|
}
|
|
@@ -193,9 +217,11 @@ export class AiFinPayAgent {
|
|
|
193
217
|
* chains.
|
|
194
218
|
*
|
|
195
219
|
* Solana key = nacl.sign.keyPair.fromSeed(seed)
|
|
196
|
-
* EVM key =
|
|
197
|
-
*
|
|
198
|
-
*
|
|
220
|
+
* EVM key = SHA-256("aifinpay:evm:v1\0" || seed)
|
|
221
|
+
* (domain-separated; independent path; not BIP-44 — see
|
|
222
|
+
* design notes in 23 - Unified SDK Design. This is the
|
|
223
|
+
* canonical derivation both SDKs implement — the Python
|
|
224
|
+
* SDK mirrors it byte-for-byte.)
|
|
199
225
|
*
|
|
200
226
|
* TODO(phase-1): replace with BIP-39/BIP-44 derivation
|
|
201
227
|
* m/44'/501'/0'/0' (Solana) + m/44'/60'/0'/0/0 (EVM) once the
|
|
@@ -215,12 +241,21 @@ export class AiFinPayAgent {
|
|
|
215
241
|
return new AiFinPayAgent(inner, evmAccount, opts);
|
|
216
242
|
}
|
|
217
243
|
/**
|
|
218
|
-
* Legacy: load the Solana side from an existing keypair
|
|
219
|
-
*
|
|
244
|
+
* Legacy: load the Solana side from an existing keypair. The EVM key is
|
|
245
|
+
* derived deterministically from the Solana secret's 32-byte seed
|
|
246
|
+
* (domain-separated SHA-256, same path as `fromSeed`) unless an explicit
|
|
247
|
+
* `opts.evmPrivateKey` is provided.
|
|
248
|
+
*
|
|
249
|
+
* Why deterministic: a random key here would change the EVM address on
|
|
250
|
+
* every process restart — anything sent to the previous address becomes
|
|
251
|
+
* unrecoverable because the key was never persisted or printed. With
|
|
252
|
+
* derivation, the same AIFINPAY_AGENT_SECRET always reproduces the same
|
|
253
|
+
* EVM address (this is what the MCP server relies on).
|
|
220
254
|
*/
|
|
221
255
|
static async fromSolanaSecret(secretB58, opts = {}) {
|
|
222
256
|
const inner = Agent.fromSecretB58(secretB58, opts);
|
|
223
|
-
const evmKey = opts.evmPrivateKey
|
|
257
|
+
const evmKey = opts.evmPrivateKey
|
|
258
|
+
?? ("0x" + bytesToHex(crypto32(inner.secretKey.subarray(0, 32))));
|
|
224
259
|
const evmAccount = privateKeyToAccount(evmKey);
|
|
225
260
|
return new AiFinPayAgent(inner, evmAccount, opts);
|
|
226
261
|
}
|
|
@@ -251,6 +286,194 @@ export class AiFinPayAgent {
|
|
|
251
286
|
}
|
|
252
287
|
return hit;
|
|
253
288
|
}
|
|
289
|
+
// ── Discovery + routing (Gateway) ───────────────────────────────────────────
|
|
290
|
+
//
|
|
291
|
+
// The vision: an agent asks AiFinPay for a *capability* ("search this",
|
|
292
|
+
// "voice this") and AiFinPay finds the best provider, pays, and returns the
|
|
293
|
+
// result. Provider selection is server-side (`/api/registry/best`) so the
|
|
294
|
+
// ranking is identical whether you call via the SDK, MCP, or raw HTTP. These
|
|
295
|
+
// methods are the convenience layer over those endpoints.
|
|
296
|
+
/**
|
|
297
|
+
* Browse the provider catalog. Filter by `category` (e.g. "search",
|
|
298
|
+
* "inference", "image", "speech") and/or free-text `q`. Each entry carries
|
|
299
|
+
* live connectivity (`status`, `latency_ms`) so you can rank client-side too.
|
|
300
|
+
*
|
|
301
|
+
* Note: this discovers *services* (the marketplace). To discover *agents*
|
|
302
|
+
* published on the public network, use `search()`.
|
|
303
|
+
*/
|
|
304
|
+
async discover(opts = {}) {
|
|
305
|
+
const base = this.inner.baseUrl;
|
|
306
|
+
const params = new URLSearchParams();
|
|
307
|
+
if (opts.category)
|
|
308
|
+
params.set("category", opts.category);
|
|
309
|
+
if (opts.q)
|
|
310
|
+
params.set("q", opts.q);
|
|
311
|
+
const r = await fetch(`${base}/api/registry?${params.toString()}`);
|
|
312
|
+
if (!r.ok)
|
|
313
|
+
throw new AiFinPayError(`discover ${base} → ${r.status}`);
|
|
314
|
+
const j = (await r.json());
|
|
315
|
+
const list = j.providers ?? [];
|
|
316
|
+
return opts.limit != null ? list.slice(0, Math.max(0, opts.limit)) : list;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Automatic provider selection. Asks the gateway for the single best
|
|
320
|
+
* available provider in a category (ranked by price, latency, liveness,
|
|
321
|
+
* on-chain trust). Throws `ProviderUnknownError` when nothing payable
|
|
322
|
+
* matches. Pass `maxPriceUsd` to cap the candidate set.
|
|
323
|
+
*/
|
|
324
|
+
async pickProvider(category, opts = {}) {
|
|
325
|
+
const base = this.inner.baseUrl;
|
|
326
|
+
const params = new URLSearchParams();
|
|
327
|
+
if (category)
|
|
328
|
+
params.set("category", category);
|
|
329
|
+
if (opts.q)
|
|
330
|
+
params.set("q", opts.q);
|
|
331
|
+
if (opts.maxPriceUsd != null)
|
|
332
|
+
params.set("max_price_usd", String(opts.maxPriceUsd));
|
|
333
|
+
const r = await fetch(`${base}/api/registry/best?${params.toString()}`);
|
|
334
|
+
if (r.status === 404) {
|
|
335
|
+
throw new ProviderUnknownError(`No available provider for category "${category}"${opts.q ? ` matching "${opts.q}"` : ""}`);
|
|
336
|
+
}
|
|
337
|
+
if (!r.ok)
|
|
338
|
+
throw new AiFinPayError(`pickProvider ${base} → ${r.status}`);
|
|
339
|
+
const j = (await r.json());
|
|
340
|
+
if (!j.best)
|
|
341
|
+
throw new ProviderUnknownError(`No best provider returned for "${category}"`);
|
|
342
|
+
return j.best;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Capability-first paid call: pick the best provider for `category`, pay it,
|
|
346
|
+
* and return the upstream response — the "agent talks only to AiFinPay" flow.
|
|
347
|
+
* Pass `opts.provider` to skip routing and force a specific provider.
|
|
348
|
+
* Resolves to `null` only when a budget cap is hit in "skip" mode (see call()).
|
|
349
|
+
*/
|
|
350
|
+
async capability(category, body, opts = {}) {
|
|
351
|
+
let providerName = opts.provider;
|
|
352
|
+
if (!providerName) {
|
|
353
|
+
const chosen = await this.pickProvider(category, { q: opts.q, maxPriceUsd: opts.maxPriceUsd });
|
|
354
|
+
providerName = chosen.name || chosen.slug;
|
|
355
|
+
}
|
|
356
|
+
return this.call({
|
|
357
|
+
provider: providerName,
|
|
358
|
+
body,
|
|
359
|
+
cost: opts.cost,
|
|
360
|
+
chain: opts.chain,
|
|
361
|
+
method: opts.method,
|
|
362
|
+
timeoutMs: opts.timeoutMs,
|
|
363
|
+
signal: opts.signal,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Web search via the best available search provider.
|
|
368
|
+
*
|
|
369
|
+
* Named `webSearch` (not `search`) on purpose: `search()` already searches
|
|
370
|
+
* the public *agent* network directory and is part of the published 1.0 API.
|
|
371
|
+
* `webSearch()` is the marketplace capability the vision calls `search()`.
|
|
372
|
+
*/
|
|
373
|
+
async webSearch(query, opts = {}) {
|
|
374
|
+
const body = typeof query === "string" ? { query } : query;
|
|
375
|
+
return this.capability("search", body, opts);
|
|
376
|
+
}
|
|
377
|
+
/** Image generation via the best available image provider. */
|
|
378
|
+
async image(prompt, opts = {}) {
|
|
379
|
+
const body = typeof prompt === "string" ? { prompt } : prompt;
|
|
380
|
+
return this.capability("image", body, opts);
|
|
381
|
+
}
|
|
382
|
+
/** Text-to-speech via the best available speech provider. */
|
|
383
|
+
async voice(text, opts = {}) {
|
|
384
|
+
const body = typeof text === "string" ? { input: text } : text;
|
|
385
|
+
return this.capability("speech", body, opts);
|
|
386
|
+
}
|
|
387
|
+
/** LLM inference via the best available inference/compute provider. */
|
|
388
|
+
async infer(body, opts = {}) {
|
|
389
|
+
return this.capability("inference", body, opts);
|
|
390
|
+
}
|
|
391
|
+
// ── Network directory ─────────────────────────────────────────────────────
|
|
392
|
+
/**
|
|
393
|
+
* Publish this agent to the public AiFinPay network so other agents can
|
|
394
|
+
* discover and call it. Self-sovereign: proves control of the EVM address by
|
|
395
|
+
* signing a one-time nonce with the agent's own key (EIP-191) — no partner
|
|
396
|
+
* account needed.
|
|
397
|
+
*/
|
|
398
|
+
async register(opts) {
|
|
399
|
+
const base = this.inner.baseUrl;
|
|
400
|
+
const addr = this.evmAddress.toLowerCase();
|
|
401
|
+
const nonce = await this.networkNonce();
|
|
402
|
+
const message = `AiFinPay-network-publish:polygon:${addr}:${nonce}`;
|
|
403
|
+
const signature = await this.evmAccount.signMessage({ message });
|
|
404
|
+
const r = await fetch(`${base}/api/network/agents/${addr}/publish`, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: { "content-type": "application/json" },
|
|
407
|
+
body: JSON.stringify({
|
|
408
|
+
name: opts.name,
|
|
409
|
+
description: opts.description ?? null,
|
|
410
|
+
endpoint: opts.endpoint,
|
|
411
|
+
capabilities: opts.capabilities ?? [],
|
|
412
|
+
pricing: opts.pricing
|
|
413
|
+
? { per_call: opts.pricing.perCall, currency: opts.pricing.currency ?? "USDC" }
|
|
414
|
+
: null,
|
|
415
|
+
nonce,
|
|
416
|
+
signature,
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
const j = (await r.json().catch(() => ({})));
|
|
420
|
+
if (!r.ok || !j.ok || !j.agent) {
|
|
421
|
+
throw new AiFinPayError(`network publish failed: ${j.error ?? r.status}`);
|
|
422
|
+
}
|
|
423
|
+
return j.agent;
|
|
424
|
+
}
|
|
425
|
+
/** Remove this agent from the public network directory (same signature proof). */
|
|
426
|
+
async unregister() {
|
|
427
|
+
const base = this.inner.baseUrl;
|
|
428
|
+
const addr = this.evmAddress.toLowerCase();
|
|
429
|
+
const nonce = await this.networkNonce();
|
|
430
|
+
const message = `AiFinPay-network-unpublish:polygon:${addr}:${nonce}`;
|
|
431
|
+
const signature = await this.evmAccount.signMessage({ message });
|
|
432
|
+
const r = await fetch(`${base}/api/network/agents/${addr}/unpublish`, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: { "content-type": "application/json" },
|
|
435
|
+
body: JSON.stringify({ nonce, signature }),
|
|
436
|
+
});
|
|
437
|
+
if (!r.ok) {
|
|
438
|
+
const j = (await r.json().catch(() => ({})));
|
|
439
|
+
throw new AiFinPayError(`network unpublish failed: ${j.error ?? r.status}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Search the public network for agents advertising a capability or matching a
|
|
444
|
+
* free-text query. Pass a bare string to search by capability
|
|
445
|
+
* (`agent.search("weather")`) or an object for finer control. Returns the
|
|
446
|
+
* directory entries; invoking one (pay + call) lands in a later release.
|
|
447
|
+
*/
|
|
448
|
+
async search(query) {
|
|
449
|
+
const base = this.inner.baseUrl;
|
|
450
|
+
const params = new URLSearchParams();
|
|
451
|
+
if (typeof query === "string") {
|
|
452
|
+
params.set("capability", query);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (query.capability)
|
|
456
|
+
params.set("capability", query.capability);
|
|
457
|
+
if (query.q)
|
|
458
|
+
params.set("q", query.q);
|
|
459
|
+
if (query.limit)
|
|
460
|
+
params.set("limit", String(query.limit));
|
|
461
|
+
}
|
|
462
|
+
const r = await fetch(`${base}/api/network/agents?${params.toString()}`);
|
|
463
|
+
if (!r.ok)
|
|
464
|
+
throw new AiFinPayError(`network search ${base} → ${r.status}`);
|
|
465
|
+
const j = (await r.json());
|
|
466
|
+
return j.agents ?? [];
|
|
467
|
+
}
|
|
468
|
+
async networkNonce() {
|
|
469
|
+
const r = await fetch(`${this.inner.baseUrl}/api/network/nonce`);
|
|
470
|
+
if (!r.ok)
|
|
471
|
+
throw new AiFinPayError(`network nonce → ${r.status}`);
|
|
472
|
+
const { nonce } = (await r.json());
|
|
473
|
+
if (!nonce)
|
|
474
|
+
throw new AiFinPayError("network nonce: empty response");
|
|
475
|
+
return nonce;
|
|
476
|
+
}
|
|
254
477
|
// ── Budget caps ─────────────────────────────────────────────────────────
|
|
255
478
|
setBudget(caps) {
|
|
256
479
|
this.budgetCaps = caps;
|
|
@@ -285,6 +508,38 @@ export class AiFinPayAgent {
|
|
|
285
508
|
getSpend24h() {
|
|
286
509
|
return this.spend24h.total24h();
|
|
287
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Sanity-check the USD estimate of an on-chain challenge amount against
|
|
513
|
+
* the declared cost and the per-call cap BEFORE signing the tx.
|
|
514
|
+
*
|
|
515
|
+
* The estimate uses env-priced MATIC/SOL (AIFINPAY_MATIC_USD /
|
|
516
|
+
* AIFINPAY_SOL_USD — same stopgap as balance()), so it is approximate.
|
|
517
|
+
* A 2x + $0.05 tolerance absorbs oracle drift while still catching the
|
|
518
|
+
* dangerous case: a bridge that quotes $0.01 in the registry and then
|
|
519
|
+
* demands 100x in the 402 challenge.
|
|
520
|
+
*
|
|
521
|
+
* Returns false (skip) instead of throwing when on_limit_exceeded="skip".
|
|
522
|
+
*/
|
|
523
|
+
guardChallengeAmount(estUsd, declaredCost, providerName) {
|
|
524
|
+
if (!Number.isFinite(estUsd) || estUsd <= 0)
|
|
525
|
+
return true; // can't estimate — don't block
|
|
526
|
+
const limits = [];
|
|
527
|
+
if (declaredCost > 0)
|
|
528
|
+
limits.push(Math.max(declaredCost * 2, declaredCost + 0.05));
|
|
529
|
+
if (this.budgetCaps.per_call_usd !== undefined)
|
|
530
|
+
limits.push(this.budgetCaps.per_call_usd);
|
|
531
|
+
if (!limits.length)
|
|
532
|
+
return true; // no cap declared anywhere — caller opted out
|
|
533
|
+
const limit = Math.min(...limits);
|
|
534
|
+
if (estUsd <= limit)
|
|
535
|
+
return true;
|
|
536
|
+
const err = new BudgetCapExceededError("per_call", `bridge ${providerName} challenge demands ≈$${estUsd.toFixed(4)} on-chain, ` +
|
|
537
|
+
`above the allowed $${limit.toFixed(4)} (declared cost/per-call cap). ` +
|
|
538
|
+
`Set AIFINPAY_MATIC_USD / AIFINPAY_SOL_USD for a tighter estimate.`);
|
|
539
|
+
if ((this.budgetCaps.on_limit_exceeded ?? "throw") === "skip")
|
|
540
|
+
return false;
|
|
541
|
+
throw err;
|
|
542
|
+
}
|
|
288
543
|
// ── Routing — chain selection ───────────────────────────────────────────
|
|
289
544
|
/**
|
|
290
545
|
* Chain selection heuristic — see Obsidian/23 - Unified SDK Design §Routing.
|
|
@@ -419,7 +674,10 @@ export class AiFinPayAgent {
|
|
|
419
674
|
async call(opts) {
|
|
420
675
|
const provider = await this.resolveProvider(opts.provider);
|
|
421
676
|
const chain = this.pickChain(provider, opts);
|
|
422
|
-
|
|
677
|
+
// Registry may carry price_usd: null — normalise to a finite number so a
|
|
678
|
+
// null/NaN cost can't poison the SpendTracker and silently disable caps.
|
|
679
|
+
const rawCost = opts.cost ?? provider.price_usd;
|
|
680
|
+
const cost = typeof rawCost === "number" && Number.isFinite(rawCost) ? rawCost : 0;
|
|
423
681
|
const withinBudget = this.checkBudget(cost);
|
|
424
682
|
if (!withinBudget)
|
|
425
683
|
return null;
|
|
@@ -439,20 +697,26 @@ export class AiFinPayAgent {
|
|
|
439
697
|
return "/run";
|
|
440
698
|
if (provider.service_type === "analytics")
|
|
441
699
|
return "/query";
|
|
700
|
+
if (provider.service_type === "image")
|
|
701
|
+
return "/images/generations";
|
|
702
|
+
if (provider.service_type === "speech")
|
|
703
|
+
return "/audio/speech";
|
|
442
704
|
return "/";
|
|
443
705
|
})();
|
|
444
706
|
const buildInit = (extraHeaders = {}) => ({
|
|
445
707
|
method: opts.method ?? "POST",
|
|
446
708
|
headers: { "content-type": "application/json", ...extraHeaders },
|
|
447
709
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
448
|
-
|
|
710
|
+
// CallOptions.timeoutMs was previously declared but never applied.
|
|
711
|
+
signal: opts.signal
|
|
712
|
+
?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined),
|
|
449
713
|
});
|
|
450
714
|
// 1. Initial unauthenticated POST → expect 402 challenge from the bridge.
|
|
451
715
|
const fullUrl = url.replace(/\/$/, "") + path;
|
|
452
716
|
const initialResp = await this.inner.fetchImpl(fullUrl, buildInit());
|
|
453
717
|
if (initialResp.status !== 402) {
|
|
454
718
|
// Bridge didn't ask for payment — pass response through unchanged.
|
|
455
|
-
|
|
719
|
+
// Nothing was paid, so the call does NOT count against budget caps.
|
|
456
720
|
if (this.telemetry)
|
|
457
721
|
this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, free: true });
|
|
458
722
|
return initialResp;
|
|
@@ -472,6 +736,17 @@ export class AiFinPayAgent {
|
|
|
472
736
|
`BRIDGE_MERCHANT_SOLANA on the bridge service.`);
|
|
473
737
|
}
|
|
474
738
|
const ps = challenge.pay_solana;
|
|
739
|
+
// Guard: the bridge controls the challenge — verify the demanded
|
|
740
|
+
// amount is in the same ballpark as the declared cost / caps before
|
|
741
|
+
// signing anything. Stops a misquoting bridge from draining the agent.
|
|
742
|
+
{
|
|
743
|
+
const lamports = Number(ps.total_lamports ?? ps.merchant_amount_lamports);
|
|
744
|
+
const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
|
|
745
|
+
const estUsd = (Number.isFinite(lamports) ? lamports / 1e9 : 0) * solUsd;
|
|
746
|
+
const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
|
|
747
|
+
if (!guarded)
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
475
750
|
const solTxSig = await this.submitSolanaB2BPayWithSplit(ps);
|
|
476
751
|
const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
|
|
477
752
|
"x-solana-tx": solTxSig,
|
|
@@ -484,6 +759,12 @@ export class AiFinPayAgent {
|
|
|
484
759
|
this.spend24h.add(cost);
|
|
485
760
|
if (this.telemetry)
|
|
486
761
|
this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: solTxSig });
|
|
762
|
+
// @internal — attach settlement metadata for consumers (MCP, telemetry
|
|
763
|
+
// dashboards) that need to surface the tx hash without an extra RPC
|
|
764
|
+
// call. Response headers are read-only after construction so we use
|
|
765
|
+
// an instance property + cast on the read side.
|
|
766
|
+
paidResp.aifinpayTx = solTxSig;
|
|
767
|
+
paidResp.aifinpayChain = "solana";
|
|
487
768
|
return paidResp;
|
|
488
769
|
}
|
|
489
770
|
// ── Polygon branch (B2BSplitter.payMatic atomic split, legacy default) ──
|
|
@@ -491,7 +772,24 @@ export class AiFinPayAgent {
|
|
|
491
772
|
throw new X402Error(`bridge ${provider.name} returned 402 but no pay_matic block — only legacy AiFinPay/Coinbase facilitators not yet wired into AiFinPayAgent.call()`);
|
|
492
773
|
}
|
|
493
774
|
const pm = challenge.pay_matic;
|
|
775
|
+
// Guard: same ballpark check as the Solana branch — never sign for an
|
|
776
|
+
// amount wildly above the declared cost / per-call cap.
|
|
777
|
+
{
|
|
778
|
+
const wei = Number(pm.total_wei);
|
|
779
|
+
const maticUsd = parseFloat(process.env.AIFINPAY_MATIC_USD ?? "0.70");
|
|
780
|
+
const estUsd = (Number.isFinite(wei) ? wei / 1e18 : 0) * maticUsd;
|
|
781
|
+
const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
|
|
782
|
+
if (!guarded)
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
494
785
|
// 2. Submit B2BSplitter.payMatic on Polygon mainnet.
|
|
786
|
+
// ipCreator routing: prefer the challenge's explicit ip_creator; else
|
|
787
|
+
// route the royalty slot to the splitter's treasury (mirrors the Solana
|
|
788
|
+
// branch). Passing address(0) would skip the transfer and permanently
|
|
789
|
+
// strand the 1bp inside B2BSplitter — the contract has no sweep function.
|
|
790
|
+
const ipCreator = pm.ip_creator
|
|
791
|
+
?? await this.splitterTreasury(pm.splitter)
|
|
792
|
+
?? "0x0000000000000000000000000000000000000000";
|
|
495
793
|
const { publicClient, walletClient } = this.polygonClients();
|
|
496
794
|
const txHash = await walletClient.writeContract({
|
|
497
795
|
address: pm.splitter,
|
|
@@ -499,7 +797,7 @@ export class AiFinPayAgent {
|
|
|
499
797
|
functionName: "payMatic",
|
|
500
798
|
args: [
|
|
501
799
|
pm.merchant_wallet,
|
|
502
|
-
|
|
800
|
+
ipCreator,
|
|
503
801
|
pm.order_id,
|
|
504
802
|
],
|
|
505
803
|
value: BigInt(pm.total_wei),
|
|
@@ -523,6 +821,9 @@ export class AiFinPayAgent {
|
|
|
523
821
|
this.spend24h.add(cost);
|
|
524
822
|
if (this.telemetry)
|
|
525
823
|
this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: txHash });
|
|
824
|
+
// @internal — see Solana branch above for rationale on property-attach.
|
|
825
|
+
paidResp.aifinpayTx = txHash;
|
|
826
|
+
paidResp.aifinpayChain = "polygon";
|
|
526
827
|
return paidResp;
|
|
527
828
|
}
|
|
528
829
|
// ── Solana b2b_pay_with_split — build, sign, send via @solana/web3.js ───
|
|
@@ -606,13 +907,17 @@ export class AiFinPayAgent {
|
|
|
606
907
|
const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
|
|
607
908
|
const polygon = await this.fetchPolygonNative().catch(() => 0);
|
|
608
909
|
const solana = await this.fetchSolanaNative().catch(() => 0);
|
|
910
|
+
const solana_usdc = await this.fetchSolanaUsdc().catch(() => 0);
|
|
911
|
+
const polygon_usdc = await this.fetchPolygonUsdc().catch(() => 0);
|
|
609
912
|
const polygonUsd = polygon * maticUsd;
|
|
610
913
|
const solanaUsd = solana * solUsd;
|
|
914
|
+
const solanaUsdcUsd = solana_usdc; // USDC ≈ $1
|
|
915
|
+
const polygonUsdcUsd = polygon_usdc; // USDC ≈ $1
|
|
611
916
|
return {
|
|
612
|
-
agent_balance_usd: polygonUsd + solanaUsd,
|
|
917
|
+
agent_balance_usd: polygonUsd + solanaUsd + solanaUsdcUsd + polygonUsdcUsd,
|
|
613
918
|
chains: {
|
|
614
|
-
solana: { sol: solana, usdc:
|
|
615
|
-
polygon: { matic: polygon, usdc:
|
|
919
|
+
solana: { sol: solana, usdc: solana_usdc, msecco_balance: 0 },
|
|
920
|
+
polygon: { matic: polygon, usdc: polygon_usdc, msecco_balance: 0 },
|
|
616
921
|
},
|
|
617
922
|
spend_24h_usd: this.spend24h.total24h(),
|
|
618
923
|
budget_caps: this.budgetCaps,
|
|
@@ -627,8 +932,9 @@ export class AiFinPayAgent {
|
|
|
627
932
|
return Number(wei) / 1e18;
|
|
628
933
|
}
|
|
629
934
|
async fetchSolanaNative() {
|
|
630
|
-
//
|
|
631
|
-
|
|
935
|
+
// Raw JSON-RPC; honours the constructor's solanaRpc option (previously
|
|
936
|
+
// re-read env here with a different default endpoint).
|
|
937
|
+
const rpc = this.solanaRpc;
|
|
632
938
|
const r = await this.inner.fetchImpl(rpc, {
|
|
633
939
|
method: "POST",
|
|
634
940
|
headers: { "content-type": "application/json" },
|
|
@@ -645,6 +951,91 @@ export class AiFinPayAgent {
|
|
|
645
951
|
const lamports = j.result?.value ?? 0;
|
|
646
952
|
return lamports / 1e9;
|
|
647
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Sum SPL USDC balances across all token accounts owned by the agent's
|
|
956
|
+
* Solana pubkey. Uses raw JSON-RPC `getTokenAccountsByOwner` so we don't
|
|
957
|
+
* pull `@solana/spl-token` as a dep. Returns 0 (never throws) when the
|
|
958
|
+
* RPC is unreachable or returns nothing — `balance()` must remain a
|
|
959
|
+
* non-blocking introspection call.
|
|
960
|
+
*/
|
|
961
|
+
async fetchSolanaUsdc() {
|
|
962
|
+
const rpc = this.solanaRpc;
|
|
963
|
+
try {
|
|
964
|
+
const r = await this.inner.fetchImpl(rpc, {
|
|
965
|
+
method: "POST",
|
|
966
|
+
headers: { "content-type": "application/json" },
|
|
967
|
+
body: JSON.stringify({
|
|
968
|
+
jsonrpc: "2.0",
|
|
969
|
+
id: 1,
|
|
970
|
+
method: "getTokenAccountsByOwner",
|
|
971
|
+
params: [
|
|
972
|
+
this.solanaAddress,
|
|
973
|
+
{ mint: USDC_SOLANA_MINT },
|
|
974
|
+
{ encoding: "jsonParsed" },
|
|
975
|
+
],
|
|
976
|
+
}),
|
|
977
|
+
});
|
|
978
|
+
if (!r.ok)
|
|
979
|
+
return 0;
|
|
980
|
+
const j = (await r.json());
|
|
981
|
+
// Sum all USDC token accounts (usually just one, but be safe)
|
|
982
|
+
const accounts = j.result?.value ?? [];
|
|
983
|
+
let total = 0;
|
|
984
|
+
for (const acc of accounts) {
|
|
985
|
+
const ui = acc.account?.data?.parsed?.info?.tokenAmount?.uiAmount;
|
|
986
|
+
if (typeof ui === "number")
|
|
987
|
+
total += ui;
|
|
988
|
+
}
|
|
989
|
+
return total;
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return 0; // never block balance() on Solana RPC issues
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Cache: splitter address → treasury address (constant per deployment).
|
|
996
|
+
splitterTreasuryCache = new Map();
|
|
997
|
+
/** Read + cache B2BSplitter.treasury(). Returns null on RPC failure. */
|
|
998
|
+
async splitterTreasury(splitter) {
|
|
999
|
+
const cached = this.splitterTreasuryCache.get(splitter.toLowerCase());
|
|
1000
|
+
if (cached)
|
|
1001
|
+
return cached;
|
|
1002
|
+
try {
|
|
1003
|
+
const { publicClient } = this.polygonClients();
|
|
1004
|
+
const treasury = await publicClient.readContract({
|
|
1005
|
+
address: splitter,
|
|
1006
|
+
abi: SPLITTER_TREASURY_ABI,
|
|
1007
|
+
functionName: "treasury",
|
|
1008
|
+
});
|
|
1009
|
+
if (!treasury || treasury === "0x0000000000000000000000000000000000000000")
|
|
1010
|
+
return null;
|
|
1011
|
+
this.splitterTreasuryCache.set(splitter.toLowerCase(), treasury);
|
|
1012
|
+
return treasury;
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Read the native Polygon USDC (Circle-issued, 6 decimals) balance for the
|
|
1020
|
+
* agent's EVM address via the existing viem publicClient. Returns 0
|
|
1021
|
+
* (never throws) on RPC failure — see fetchSolanaUsdc rationale.
|
|
1022
|
+
*/
|
|
1023
|
+
async fetchPolygonUsdc() {
|
|
1024
|
+
try {
|
|
1025
|
+
const { publicClient } = this.polygonClients();
|
|
1026
|
+
const raw = await publicClient.readContract({
|
|
1027
|
+
address: USDC_POLYGON_ERC20,
|
|
1028
|
+
abi: ERC20_BALANCE_OF_ABI,
|
|
1029
|
+
functionName: "balanceOf",
|
|
1030
|
+
args: [this.evmAccount.address],
|
|
1031
|
+
});
|
|
1032
|
+
// USDC has 6 decimals on Polygon
|
|
1033
|
+
return Number(raw) / 1e6;
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
648
1039
|
// ── Reputation ──────────────────────────────────────────────────────────
|
|
649
1040
|
/**
|
|
650
1041
|
* TODO(phase-4 / phase-5): query operator backend reputation engine.
|
|
@@ -695,15 +1086,10 @@ function bytesToHex(b) {
|
|
|
695
1086
|
* before audit-grade derivation lands.
|
|
696
1087
|
*/
|
|
697
1088
|
function crypto32(seed) {
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// Fall through to the node:crypto path.
|
|
703
|
-
}
|
|
704
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
705
|
-
const nodeCrypto = require("node:crypto");
|
|
706
|
-
const h = nodeCrypto.createHash("sha256");
|
|
1089
|
+
// Must stay sync (constructors call it). Uses the createHash imported at
|
|
1090
|
+
// the top of this file — a bare `require()` is a ReferenceError here
|
|
1091
|
+
// because this package ships as ESM ("type": "module").
|
|
1092
|
+
const h = createHash("sha256");
|
|
707
1093
|
h.update("aifinpay:evm:v1\0");
|
|
708
1094
|
h.update(seed);
|
|
709
1095
|
return new Uint8Array(h.digest());
|