import { PaymentSDK } from "./PaymentSDK"; import { IdentitySDK } from "./IdentitySDK"; import { PauliKeySDK } from "./PauliKeySDK"; import type { FluidAgentConfig, AgentMeResult } from "./types"; const DEFAULT_BASE_URL = "https://fluidnative.com"; const FADP_HEADER_REQ = "x-fadp-required"; const FADP_HEADER_PROOF = "X-FADP-Proof"; interface FADPPaymentRequired { version: string; amount: string; token: string; chain: string; payTo: string; description?: string; nonce: string; expires: number; } interface BalanceEntry { token: string; amount: string; chain?: string; } interface BalanceResult { balances: BalanceEntry[]; walletAddress: string | null; usdc: number; } export class FluidAgent { readonly payments: PaymentSDK; readonly identity: IdentitySDK; readonly keys: PauliKeySDK; private agentKey: string; private baseUrl: string; private fadpEnabled: boolean; private fadpMaxUsd: number; private checkBalanceEnabled: boolean; constructor(config: FluidAgentConfig) { if (!config.agentKey || !config.agentKey.startsWith("fwag_")) { throw new Error( "Invalid agent key — keys must start with fwag_. " + "Create one at fluidnative.com/agentic-keys" ); } this.agentKey = config.agentKey; this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""); this.fadpEnabled = config.fadp ?? false; this.fadpMaxUsd = config.fadpMaxUsd ?? 10; this.checkBalanceEnabled = config.checkBalanceBeforePayment ?? false; const fetcher = this._fetch.bind(this); this.payments = new PaymentSDK(fetcher); this.identity = new IdentitySDK(fetcher); this.keys = new PauliKeySDK(fetcher); } // ─── Internal authenticated fetch ────────────────────────────────────────── private async _fetch(path: string, init?: RequestInit): Promise { const url = `${this.baseUrl}${path}`; const headers: Record = { "Content-Type": "application/json", "X-Agent-Key": this.agentKey, ...((init?.headers as Record) ?? {}), }; return fetch(url, { ...init, headers }); } // ─── Balance (parsed) ─────────────────────────────────────────────────────── /** * Returns balance entries plus a pre-parsed USDC amount for the given chain. */ private async _getBalance(chain = "base"): Promise { try { const r = await this._fetch(`/v1/agents/balance?chain=${encodeURIComponent(chain)}`); if (!r.ok) return { balances: [], walletAddress: null, usdc: 0 }; const data = await r.json() as any; const balances: BalanceEntry[] = data.balances ?? []; const walletAddress: string | null = data.walletAddress ?? null; const usdcEntry = balances.find( b => b.token?.toUpperCase() === "USDC" && (!b.chain || b.chain === chain) ); const usdc = parseFloat(usdcEntry?.amount ?? "0"); return { balances, walletAddress, usdc }; } catch { return { balances: [], walletAddress: null, usdc: 0 }; } } // ─── Balance pre-check ────────────────────────────────────────────────────── /** * Checks if the wallet has enough balance to pay. * Logs a funding message if insufficient and throws. */ private async _assertSufficientBalance( amount: string, token: string, chain: string ): Promise { const isStable = ["USDC", "USDT", "DAI"].includes(token.toUpperCase()); if (!isStable) return; // only check stable coins — ETH needs gas estimation const { balances, walletAddress, usdc } = await this._getBalance(chain); const needed = parseFloat(amount); console.log("[FluidAgent] Wallet balance before payment:"); for (const b of balances) { console.log(` ${b.token}: ${b.amount}${b.chain ? ` (${b.chain})` : ""}`); } if (usdc < needed) { const short = (needed - usdc).toFixed(6); const lines = [ `[FluidAgent] Insufficient USDC balance.`, ` Have: ${usdc.toFixed(6)} USDC`, ` Need: ${needed.toFixed(6)} USDC`, ` Short: ${short} USDC`, ``, ` Fund your wallet on Base mainnet:`, ` Token contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (USDC on Base)`, walletAddress ? ` Send at least ${short} USDC to: ${walletAddress}` : ` Log in to fluidnative.com → Settings to find your wallet address`, ]; console.error(lines.join("\n")); throw new Error( `[FluidAgent] Insufficient USDC: have ${usdc.toFixed(6)}, need ${needed.toFixed(6)}. ` + (walletAddress ? `Fund ${walletAddress} on Base mainnet.` : "") ); } console.log(`[FluidAgent] Balance OK — ${usdc.toFixed(6)} USDC available`); } // ─── Receipt logger ───────────────────────────────────────────────────────── private _logReceipt(payData: any): void { const { txHash, explorerUrl, receipt } = payData; if (!txHash) return; console.log("[FluidAgent] ┌─ PAYMENT RECEIPT ────────────────────────────────────"); if (receipt) { console.log(`[FluidAgent] │ Receipt ID: ${receipt.id ?? "—"}`); console.log(`[FluidAgent] │ Protocol: ${receipt.protocol ?? "FADP/1.0"}`); console.log(`[FluidAgent] │ Network: ${receipt.network ?? "Base Mainnet"} (Chain ${receipt.chainId ?? 8453})`); console.log(`[FluidAgent] │ Timestamp: ${receipt.timestamp ?? new Date().toISOString()}`); console.log(`[FluidAgent] │`); if (receipt.from) { console.log(`[FluidAgent] │ From (sender)`); if (receipt.from.uoi) console.log(`[FluidAgent] │ UOI: ${receipt.from.uoi}`); if (receipt.from.address) console.log(`[FluidAgent] │ Address: ${receipt.from.address}`); if (receipt.from.email) console.log(`[FluidAgent] │ Email: ${receipt.from.email}`); } if (receipt.to) { console.log(`[FluidAgent] │`); console.log(`[FluidAgent] │ To (recipient)`); if (receipt.to.uoi) console.log(`[FluidAgent] │ UOI: ${receipt.to.uoi}`); if (receipt.to.address) console.log(`[FluidAgent] │ Address: ${receipt.to.address}`); } if (receipt.payment) { console.log(`[FluidAgent] │`); console.log(`[FluidAgent] │ Amount: ${receipt.payment.amount} ${receipt.payment.token}`); if (receipt.payment.tokenAddress) { console.log(`[FluidAgent] │ Token Addr: ${receipt.payment.tokenAddress}`); } } } console.log(`[FluidAgent] │`); console.log(`[FluidAgent] │ Tx Hash: ${txHash}`); const link = explorerUrl ?? `https://basescan.org/tx/${txHash}`; console.log(`[FluidAgent] │ BaseScan: ${link}`); console.log(`[FluidAgent] └────────────────────────────────────────────────────`); } // ─── Shared FADP request logic ───────────────────────────────────────────── private async _fadpRequest( input: string | URL, init: RequestInit | undefined, maxUsd: number, forceEnabled = false, ): Promise { const firstRes = await fetch(input, init); if (firstRes.status !== 402) return firstRes; const fadpHeader = firstRes.headers.get(FADP_HEADER_REQ); if (!fadpHeader) return firstRes; if (!this.fadpEnabled && !forceEnabled) { console.warn( "[FluidAgent] FADP payment wall detected. Set fadp: true or use agent.fadpFetch().\n" + ` X-FADP-Required: ${fadpHeader}` ); return firstRes; } let required: FADPPaymentRequired; try { required = JSON.parse(fadpHeader) as FADPPaymentRequired; } catch { throw new Error("[FluidAgent] Could not parse X-FADP-Required header"); } const amount = parseFloat(required.amount); const isStable = ["USDC", "USDT", "DAI"].includes(required.token.toUpperCase()); const estimatedUsd = isStable ? amount : amount * 3000; if (estimatedUsd > maxUsd) { throw new Error( `[FluidAgent] FADP payment of ${required.amount} ${required.token} (~$${estimatedUsd.toFixed(2)}) ` + `exceeds maxAutoPayUsd limit of $${maxUsd}. Raise fadpMaxUsd in config to allow.` ); } console.log( `[FluidAgent] FADP: paying ${required.amount} ${required.token} on ${required.chain}` + (required.description ? ` — "${required.description}"` : "") ); // ── Balance pre-check (optional, enabled by checkBalanceBeforePayment) ── if (this.checkBalanceEnabled) { await this._assertSufficientBalance(required.amount, required.token, required.chain); } const payRes = await this._fetch("/v1/agents/send", { method: "POST", body: JSON.stringify({ to: required.payTo, amount: required.amount, token: required.token, chain: required.chain, }), }); const payData = await payRes.json() as any; if (!payRes.ok) throw new Error(`[FluidAgent] FADP payment failed: ${payData.error ?? payRes.statusText}`); if (!payData.txHash) throw new Error("[FluidAgent] FADP payment returned no txHash"); // ── Full receipt log ───────────────────────────────────────────────────── this._logReceipt(payData); const proof = JSON.stringify({ txHash: payData.txHash, agentKeyPrefix: this.agentKey.slice(0, 12), nonce: required.nonce, timestamp: Math.floor(Date.now() / 1000), }); const retryHeaders: Record = { ...((init?.headers as Record) ?? {}), [FADP_HEADER_PROOF]: proof, }; return fetch(input, { ...init, headers: retryHeaders }); } // ─── FADP-aware external fetch (single call) ────────────────────────────── /** * FADP-aware fetch — call any external API and auto-pay FADP 402 walls. * * Requires `fadp: true` in config (or use fadpFetch() for per-call config). * * @example * const agent = new FluidAgent({ agentKey: "fwag_...", fadp: true }); * const res = await agent.fetch("https://paid-api.example.com/data"); */ async fetch(input: string | URL, init?: RequestInit): Promise { return this._fadpRequest(input, init, this.fadpMaxUsd); } // ─── FADP-aware fetch factory ────────────────────────────────────────────── /** * Returns a reusable FADP-aware fetch function. * Always auto-pays regardless of the `fadp` config flag. * * @example * // Type B — TypeScript project * const fetch = agent.fadpFetch({ maxAutoPayUsd: 5 }); * const res = await fetch("https://paid-api.example.com/data"); * * // Type C — Full-stack: pass to a service * const safeFetch = agent.fadpFetch(); * const data = await safeFetch(apiUrl).then(r => r.json()); */ fadpFetch(opts?: { maxAutoPayUsd?: number }) { const maxUsd = opts?.maxAutoPayUsd ?? this.fadpMaxUsd; return (input: string | URL, init?: RequestInit): Promise => this._fadpRequest(input, init, maxUsd, true); } // ─── Agent identity ───────────────────────────────────────────────────────── /** Get identity, email, scopes, and key metadata for the current agent key */ async me(): Promise { const r = await this._fetch("/v1/agents/me"); const data = await r.json() as any; if (!r.ok) throw new Error(data.error ?? "auth failed"); return data as AgentMeResult; } // ─── Wallet balance ───────────────────────────────────────────────────────── /** * USDC (and ETH) balance for the current agent's wallet. * * @param chainOrOpts Chain name "base" | "ethereum" | "solana" * or an options object { chain?: string } * * @example * await agent.getBalance("base"); * await agent.getBalance({ chain: "base" }); */ async getBalance(chainOrOpts?: string | { chain?: string }): Promise { const chain = typeof chainOrOpts === "string" ? chainOrOpts : chainOrOpts?.chain; const path = chain ? `/v1/agents/balance?chain=${encodeURIComponent(chain)}` : "/v1/agents/balance"; const r = await this._fetch(path); const data = await r.json() as any; if (!r.ok) throw new Error(data.error ?? "balance fetch failed"); return data; } }