/** * Fluid Agent Skills — framework-compatible LLM tool definitions. * * Works out-of-the-box with: * Claude (Anthropic) · GPT (OpenAI) · Vercel AI SDK · LangChain / LangGraph * * Usage: * const agent = new FluidAgent({ agentKey: process.env.FLUID_AGENT_KEY }); * const tools = toAnthropicTools(agent); // pass to Claude * const tools = toOpenAITools(agent); // pass to GPT */ import type { FluidAgent } from "./FluidAgent"; // ─── Universal tool definition ───────────────────────────────────────────── export interface FluidTool { name: string; description: string; inputSchema: { type: "object"; properties: Record; required?: string[]; }; run(args: Record): Promise; } // ─── All Fluid tools ─────────────────────────────────────────────────────── export function getFluidTools(agent: FluidAgent): FluidTool[] { return [ // ── Identity ───────────────────────────────────────────────────────── { name: "fluid_me", description: "Get the identity of the current Fluid agent: email, wallet address, key name, and granted scopes. " + "Call this first to understand what the agent is authorised to do.", inputSchema: { type: "object", properties: {} }, async run() { return JSON.stringify(await agent.me()); }, }, { name: "fluid_balance", description: "Get the on-chain USDC (and ETH) balance of the agent's wallet. " + "Specify chain to check balances on Base, Ethereum, or Solana.", inputSchema: { type: "object", properties: { chain: { type: "string", description: "base | ethereum | solana (default: base)" }, }, }, async run(args) { return JSON.stringify(await agent.getBalance(args.chain as string | undefined)); }, }, { name: "fluid_history", description: "Get recent swap and send transaction history for the agent's wallet.", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Max transactions to return (1–100, default 20)" }, }, }, async run(args) { const limit = typeof args.limit === "number" ? args.limit : 20; return JSON.stringify(await agent.identity.history(limit)); }, }, // ── UAI / Fluid ID ──────────────────────────────────────────────────── { name: "fluid_resolve", description: "Resolve a Fluid Wallet user's email address to their on-chain wallet address. " + "Use this before fluid_send when you have an email but need an address.", inputSchema: { type: "object", required: ["email"], properties: { email: { type: "string", description: "Email address of the Fluid Wallet user" }, }, }, async run(args) { return JSON.stringify(await agent.identity.resolve(args.email as string)); }, }, { name: "fluid_uai_resolve", description: "Resolve a Fluid ID username to a wallet address. " + "Fluid IDs are human-readable names (e.g. 'alice', 'myapp') registered in the Fluid Name Service. " + "Use this to look up any registered username.", inputSchema: { type: "object", required: ["username"], properties: { username: { type: "string", description: "Fluid ID username without suffix, e.g. 'alice'" }, networkId: { type: "string", description: "Chain filter: base | ethereum | solana (optional)" }, }, }, async run(args) { return JSON.stringify( await agent.identity.resolveFluidId(args.username as string, args.networkId as string | undefined) ); }, }, { name: "fluid_uai_reverse", description: "Reverse-resolve a wallet address to its registered Fluid ID. " + "Returns null if the address has no Fluid ID registered.", inputSchema: { type: "object", required: ["address"], properties: { address: { type: "string", description: "EVM (0x…) or Solana wallet address" }, }, }, async run(args) { return JSON.stringify(await agent.identity.reverseFluidId(args.address as string)); }, }, // ── Prices & quotes ─────────────────────────────────────────────────── { name: "fluid_price", description: "Get the current USD spot price for any supported token. " + "Aggregates prices from multiple on-chain sources.", inputSchema: { type: "object", required: ["token"], properties: { token: { type: "string", description: "Token symbol: ETH, BTC, SOL, USDC, MATIC, etc." }, }, }, async run(args) { return JSON.stringify(await agent.identity.getPrice(args.token as string)); }, }, { name: "fluid_quote_swap", description: "Get a live price quote for swapping tokens via FluidSOR — 25+ DEX venues including " + "Fluid AMM, Uniswap V3, Aerodrome, Jupiter. Call this before fluid_swap to check rates. " + "Returns ranked routes with amounts, price impact, and gas estimates.", inputSchema: { type: "object", required: ["fromToken", "toToken", "amount"], properties: { fromToken: { type: "string", description: "Token to sell (e.g. ETH, USDC, SOL)" }, toToken: { type: "string", description: "Token to buy (e.g. USDC, USDT, WETH)" }, amount: { type: "string", description: "Amount to sell as a string, e.g. '0.1'" }, chain: { type: "string", description: "base | ethereum | solana | injective (default: base)" }, }, }, async run(args) { return JSON.stringify(await agent.payments.quoteSwap({ fromToken: args.fromToken as string, toToken: args.toToken as string, amount: args.amount as string, chain: args.chain as string | undefined, })); }, }, { name: "fluid_estimate_gas", description: "Estimate the gas cost in USD for a token send before executing it.", inputSchema: { type: "object", required: ["to", "amount"], properties: { to: { type: "string", description: "Recipient wallet address (0x…)" }, amount: { type: "string", description: "Amount to send, e.g. '0.01'" }, token: { type: "string", description: "Token symbol: ETH | USDC | USDT (default: ETH)" }, chain: { type: "string", description: "base | ethereum (default: base)" }, }, }, async run(args) { return JSON.stringify(await agent.payments.estimateGas({ to: args.to as string, amount: args.amount as string, token: args.token as string | undefined, chain: args.chain as string | undefined, })); }, }, // ── Payments ────────────────────────────────────────────────────────── { name: "fluid_send", description: "Send ETH or ERC-20 tokens on-chain. Requires the 'pay' scope. " + "Transactions above $100 (by default) return status 'pending_approval' — " + "use fluid_approval_status with the returned approvalToken to poll until approved.", inputSchema: { type: "object", required: ["to", "amount"], properties: { to: { type: "string", description: "Recipient wallet address (0x…) or Fluid ID username" }, amount: { type: "string", description: "Amount to send, e.g. '10.5'" }, token: { type: "string", description: "Token symbol: ETH | USDC | USDT (default: ETH)" }, chain: { type: "string", description: "base | ethereum | solana (default: base)" }, }, }, async run(args) { return JSON.stringify(await agent.payments.send({ to: args.to as string, amount: args.amount as string, token: args.token as string | undefined, chain: args.chain as string | undefined, })); }, }, { name: "fluid_batch_send", description: "Send USDC to multiple recipients in a single call. " + "Ideal for tournament payouts, reward distributions, airdrops, and game prize pools. " + "Returns a per-recipient breakdown of successes and failures. " + "Requires the 'pay' scope.", inputSchema: { type: "object", required: ["recipients"], properties: { recipients: { type: "array", description: "List of recipients with to, amount, and optional label", items: { type: "object", }, }, chain: { type: "string", description: "base | ethereum | solana (default: base)" }, token: { type: "string", description: "Token to send (default: USDC)" }, }, }, async run(args) { return JSON.stringify(await agent.payments.batchSend({ recipients: args.recipients as any[], chain: args.chain as string | undefined, token: args.token as string | undefined, })); }, }, { name: "fluid_swap", description: "Execute a token swap via FluidSOR (best price across Fluid AMM, Uniswap V3, Aerodrome, Jupiter). " + "Call fluid_quote_swap first to see the rate. Requires the 'swap' scope. " + "If you receive an OTP_REQUIRED error, ask the wallet owner for their TOTP code and retry with otpCode.", inputSchema: { type: "object", required: ["fromToken", "toToken", "amount"], properties: { fromToken: { type: "string", description: "Token to sell (e.g. ETH)" }, toToken: { type: "string", description: "Token to buy (e.g. USDC)" }, amount: { type: "string", description: "Amount to sell, e.g. '0.1'" }, slippage: { type: "string", description: "Slippage tolerance in percent (default: 0.5)" }, chain: { type: "string", description: "base | ethereum | solana (default: base)" }, otpCode: { type: "string", description: "6-digit TOTP code from the wallet owner's authenticator app — required when account has TOTP enabled" }, }, }, async run(args) { return JSON.stringify(await agent.payments.swap({ fromToken: args.fromToken as string, toToken: args.toToken as string, amount: args.amount as string, slippage: args.slippage as string | undefined, chain: args.chain as string | undefined, otpCode: args.otpCode as string | undefined, })); }, }, { name: "fluid_agent_pay", description: "Pay another Fluid Wallet user directly by their email address — no wallet address needed. " + "Fluid resolves the address automatically. Requires the 'agentpay' scope.", inputSchema: { type: "object", required: ["toEmail", "amount"], properties: { toEmail: { type: "string", description: "Recipient's Fluid Wallet email" }, amount: { type: "string", description: "Amount to send, e.g. '10'" }, token: { type: "string", description: "Token: USDC | ETH (default: USDC)" }, memo: { type: "string", description: "Optional payment memo or reference" }, }, }, async run(args) { return JSON.stringify(await agent.payments.agentPay({ toEmail: args.toEmail as string, amount: args.amount as string, token: args.token as string | undefined, memo: args.memo as string | undefined, })); }, }, { name: "fluid_approval_status", description: "Poll the approval status of a high-value transaction that required wallet-owner approval. " + "When fluid_send returns status 'pending_approval', use the approvalToken here. " + "Poll every ~10 seconds until status is 'approved' or 'rejected'.", inputSchema: { type: "object", required: ["approvalToken"], properties: { approvalToken: { type: "string", description: "The approvalToken returned by fluid_send" }, }, }, async run(args) { return JSON.stringify(await agent.payments.approvalStatus({ approvalToken: args.approvalToken as string, })); }, }, // ── FADP ────────────────────────────────────────────────────────────── { name: "fluid_fadp_fetch", description: "Call any external API URL that may be gated behind an FADP (Fluid Agentic Payment Protocol) " + "HTTP 402 payment wall. The agent automatically pays and retries. " + "Requires fadp: true in FluidAgentConfig and the 'pay' scope.", inputSchema: { type: "object", required: ["url"], properties: { url: { type: "string", description: "The URL to fetch (may be FADP-gated)" }, method: { type: "string", description: "HTTP method: GET | POST | PUT (default: GET)" }, body: { type: "string", description: "Request body as JSON string (for POST/PUT)" }, }, }, async run(args) { const init: RequestInit = { method: (args.method as string | undefined) ?? "GET", headers: { "Content-Type": "application/json" }, ...(args.body ? { body: args.body as string } : {}), }; const res = await agent.fetch(args.url as string, init); const text = await res.text(); try { return JSON.stringify({ status: res.status, body: JSON.parse(text) }); } catch { return JSON.stringify({ status: res.status, body: text }); } }, }, // ── Real-world agent scenario skills ────────────────────────────────── // // These five tools map directly to how shipped AI agents use payments: // // fluid_pay_service → Replit Agent "Pay for two MJ movie tickets" // fluid_buy_movie_tickets → Replit Agent "Buy 2 AMC IMAX seats for Mission Impossible Saturday" // fluid_fetch_paid_api → Claude "Read this 80-page contract" (paid API) // fluid_pay_order → ChatGPT/Operator "Order groceries from Whole Foods" // fluid_delegate_task → Devin "Debug staging, open a PR" // fluid_research_pay → Manus "Research top agentic fintechs, build sheet" { name: "fluid_pay_service", description: "Pay for any real-world service, ticket, subscription, or digital product. " + "Use this when the user asks an agent to purchase something on their behalf — " + "e.g. 'Pay for two MJ movie tickets at Fandango', 'Buy a ChatGPT Plus subscription', " + "'Pay for my Spotify renewal', 'Purchase credits on Replit'. " + "Works with any platform that has a Fluid Wallet Business Wallet, including movie " + "ticket platforms: Fandango, AMC Theatres, Regal Cinemas, Cinemark, Atom Tickets, " + "Alamo Drafthouse, and Marcus Theatres. For movie-specific purchases (seats, showtime, " + "format) use fluid_buy_movie_tickets instead — it has richer movie-specific fields. " + "Checks the wallet balance BEFORE attempting payment and refuses with a " + "funding message if insufficient. Returns a full on-chain receipt with a " + "BaseScan explorer link proving the payment happened. " + "Requires the 'pay' scope. Token defaults to USDC on Base.", inputSchema: { type: "object", required: ["to", "amount", "description"], properties: { to: { type: "string", description: "Recipient wallet address (0x…) or Fluid Wallet email" }, amount: { type: "string", description: "Amount to pay in USDC, e.g. '24.99'" }, token: { type: "string", description: "Token: USDC | ETH | USDT (default: USDC)" }, chain: { type: "string", description: "Chain: base | ethereum (default: base)" }, description: { type: "string", description: "What is being purchased, e.g. '2x MJ concert tickets'" }, memo: { type: "string", description: "Optional order ID, confirmation code, or note" }, }, }, async run(args) { const { to, amount, token = "USDC", chain = "base", description, memo } = args as Record; // ── Balance check before committing ────────────────────────────── const balData = await agent.getBalance(chain) as any; const balances: Array<{ token: string; amount: string }> = balData?.balances ?? []; const walletAddress: string | null = balData?.walletAddress ?? null; const usdcBal = parseFloat( balances.find(b => b.token?.toUpperCase() === "USDC")?.amount ?? "0" ); const needed = parseFloat(amount); if (token.toUpperCase() === "USDC" && usdcBal < needed) { return JSON.stringify({ success: false, error: "Insufficient USDC balance", balance: { available: usdcBal.toFixed(6), needed: needed.toFixed(6), short: (needed - usdcBal).toFixed(6) }, fundingInstructions: { network: "Base mainnet (Chain ID 8453)", token: "USDC", tokenContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", sendTo: walletAddress, message: `Send at least ${(needed - usdcBal).toFixed(6)} USDC to the wallet address above, then retry.`, }, }); } // ── Execute payment ─────────────────────────────────────────────── const result = await agent.payments.send({ to, amount, token, chain }); return JSON.stringify({ success: true, description, memo: memo ?? null, txHash: result.txHash, explorerUrl: result.explorerUrl ?? `https://basescan.org/tx/${result.txHash}`, receipt: result.receipt ?? null, message: `Payment of ${amount} ${token} confirmed on-chain for: ${description}`, }); }, }, { name: "fluid_buy_movie_tickets", description: "Buy movie tickets at US cinema platforms by paying the platform's Business Wallet " + "with USDC on Base mainnet. Use this when the user asks an agent to purchase movie " + "tickets — e.g. 'Buy 2 MJ tickets at AMC for Saturday 8pm', " + "'Get us 4 IMAX seats for Mission Impossible at Regal on Friday', " + "'Book Alamo Drafthouse dine-in seats for Blade Runner tonight', " + "'Pay for 2 Cinemark XD tickets for Avengers', " + "'Reserve Fandango seats for Top Gun 2 this weekend'. " + "Supported platforms (all accept USDC on Base mainnet via Business Wallet): " + "Fandango (fandango.com), AMC Theatres (amctheatres.com), " + "Regal Cinemas (regmovies.com), Cinemark (cinemark.com), " + "Atom Tickets (atomtickets.com), Alamo Drafthouse (drafthouse.com), " + "Marcus Theatres (marcustheatres.com). " + "Checks USDC balance before paying. Returns a full on-chain receipt with BaseScan proof. " + "Typical prices: standard $10–$20 USDC/seat, IMAX/RPX/XD $18–$25 USDC/seat, " + "dine-in premium $20–$30 USDC/seat. Requires 'pay' scope.", inputSchema: { type: "object", required: ["to", "amount", "platform", "movie"], properties: { to: { type: "string", description: "Platform Business Wallet address (0x…) or Fluid Wallet email. " + "Known emails: Fandango→payments@fandango.com, AMC→payments@amctheatres.com, " + "Regal→payments@regmovies.com, Cinemark→payments@cinemark.com, " + "Atom Tickets→payments@atomtickets.com, Alamo Drafthouse→payments@drafthouse.com, " + "Marcus Theatres→payments@marcustheatres.com", }, amount: { type: "string", description: "Total ticket cost in USDC, e.g. '29.98' for 2 seats at $14.99 each" }, platform: { type: "string", description: "Cinema platform name: Fandango | AMC | Regal | Cinemark | Atom Tickets | Alamo Drafthouse | Marcus Theatres" }, movie: { type: "string", description: "Full movie title, e.g. 'Mission Impossible: The Final Reckoning'" }, seats: { type: "number", description: "Number of seats / tickets to purchase" }, showtime: { type: "string", description: "Showtime as ISO 8601 or natural language, e.g. '2026-05-10T20:00' or 'Saturday 8pm'" }, format: { type: "string", description: "Screen format: standard | IMAX | Dolby | 4DX | RPX | XD | UltraScreen | SuperScreen | BistroPlex | dine-in" }, location: { type: "string", description: "Theatre name or city, e.g. 'AMC Century City' or 'Los Angeles, CA'" }, orderId: { type: "string", description: "Platform order reference number, if obtained from the checkout flow" }, token: { type: "string", description: "Token to pay with: USDC | ETH (default: USDC)" }, chain: { type: "string", description: "Chain: base | ethereum (default: base)" }, }, }, async run(args) { const { to, amount, platform, movie, seats, showtime, format, location, orderId, token = "USDC", chain = "base", } = args as Record; // ── Balance check ──────────────────────────────────────────────── const balData = await agent.getBalance(chain) as any; const balances: Array<{ token: string; amount: string }> = balData?.balances ?? []; const walletAddress: string | null = balData?.walletAddress ?? null; const usdcBal = parseFloat( balances.find(b => b.token?.toUpperCase() === "USDC")?.amount ?? "0" ); const needed = parseFloat(amount); if (token.toUpperCase() === "USDC" && usdcBal < needed) { const short = (needed - usdcBal).toFixed(6); return JSON.stringify({ success: false, error: "Insufficient USDC balance to purchase tickets", platform, movie, balance: { available: usdcBal.toFixed(6), needed: needed.toFixed(6), short, }, fundingInstructions: { message: `Send at least ${short} USDC to your Fluid Wallet on Base mainnet, then retry.`, network: "Base mainnet (Chain ID 8453)", token: "USDC", tokenContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", sendTo: walletAddress, steps: [ "1. Buy USDC on Coinbase (or any exchange)", "2. Withdraw → Base network → paste your wallet address above", "3. Wait ~10 seconds for Base confirmation", "4. Retry the ticket purchase", ], }, }); } // ── Build memo with ticket details ─────────────────────────────── const memoParts = [ seats && `${seats}x`, movie, format && `[${format}]`, location && `@ ${location}`, showtime && `| ${showtime}`, orderId && `| order#${orderId}`, ].filter(Boolean); const memo = memoParts.join(" "); // ── Execute payment → platform Business Wallet ─────────────────── const result = await agent.payments.send({ to, amount, token, chain }); const explorerUrl = result.explorerUrl ?? `https://basescan.org/tx/${result.txHash}`; return JSON.stringify({ success: true, platform, movie, seats: seats ?? null, showtime: showtime ?? null, format: format ?? null, location: location ?? null, orderId: orderId ?? null, amount: `${amount} ${token}`, txHash: result.txHash, explorerUrl, receipt: result.receipt ?? null, memo, message: `Payment confirmed on-chain! ${seats ?? ""} ticket${seats !== 1 ? "s" : ""} for "${movie}" ` + `at ${platform} paid with ${amount} ${token}. ` + `Check your email for QR codes. BaseScan proof: ${explorerUrl}`, }); }, }, { name: "fluid_fetch_paid_api", description: "Fetch data from any URL that may be gated behind a crypto payment wall (FADP/1.0). " + "Use this when an agent needs to access paid APIs, premium datasets, or document " + "processing services — e.g. 'Read this 80-page contract and flag risky clauses', " + "'Get the latest SEC filings for Apple', 'Fetch premium market data from this endpoint', " + "'Analyse this research paper via the paid API'. " + "If the API responds with HTTP 402 + FADP payment header, this tool automatically pays " + "the USDC toll and retries. Prints a full receipt with BaseScan link after paying. " + "Requires 'pay' scope if the endpoint charges. Returns the API response body as JSON or text.", inputSchema: { type: "object", required: ["url"], properties: { url: { type: "string", description: "The API or document URL to fetch (may be FADP-gated)" }, method: { type: "string", description: "HTTP method: GET | POST | PUT (default: GET)" }, body: { type: "string", description: "Request body as JSON string (for POST/PUT)" }, maxPayUsd: { type: "number", description: "Maximum USDC to auto-pay if the API charges (default: 10)" }, description: { type: "string", description: "What data you are trying to get — for logging" }, }, }, async run(args) { const { url, method = "GET", body, maxPayUsd = 10, description } = args as Record; const fadpFetch = agent.fadpFetch({ maxAutoPayUsd: Number(maxPayUsd) }); const init: RequestInit = { method, headers: { "Content-Type": "application/json" }, ...(body ? { body: typeof body === "string" ? body : JSON.stringify(body) } : {}), }; const res = await fadpFetch(url, init); const text = await res.text(); let parsed: unknown; try { parsed = JSON.parse(text); } catch { parsed = text; } return JSON.stringify({ status: res.status, ok: res.ok, description: description ?? url, data: parsed, }); }, }, { name: "fluid_pay_order", description: "Pay for a product delivery, grocery order, food order, or e-commerce purchase. " + "Use this when an agent is asked to order physical or digital goods and pay for them — " + "e.g. 'Order groceries from Whole Foods for delivery tonight', " + "'Pay for my Uber Eats order', 'Buy an iPhone case and send to my address', " + "'Pay the $38.50 Instacart order'. " + "Accepts a list of order items for the memo so the recipient knows what was ordered. " + "Checks balance first. Returns an on-chain receipt with a BaseScan explorer link. " + "Requires the 'pay' scope. Token defaults to USDC on Base.", inputSchema: { type: "object", required: ["to", "amount"], properties: { to: { type: "string", description: "Merchant wallet address (0x…) or Fluid Wallet email" }, amount: { type: "string", description: "Total order amount in USDC, e.g. '38.50'" }, token: { type: "string", description: "Token: USDC | ETH (default: USDC)" }, chain: { type: "string", description: "Chain: base | ethereum (default: base)" }, orderId: { type: "string", description: "Order ID or reference number from the merchant" }, items: { type: "array", description: "Line items in the order, e.g. [{name:'Bananas',qty:3,price:'1.29'}]", items: { type: "object" }, }, deliveryNote: { type: "string", description: "Optional delivery address or special instructions" }, }, }, async run(args) { const { to, amount, token = "USDC", chain = "base", orderId, items, deliveryNote } = args as Record; // Balance check const balData = await agent.getBalance(chain) as any; const balances: Array<{ token: string; amount: string }> = balData?.balances ?? []; const walletAddress: string | null = balData?.walletAddress ?? null; const usdcBal = parseFloat(balances.find(b => b.token?.toUpperCase() === "USDC")?.amount ?? "0"); const needed = parseFloat(amount); if (token.toUpperCase() === "USDC" && usdcBal < needed) { return JSON.stringify({ success: false, error: "Insufficient USDC balance to pay for this order", balance: { available: usdcBal.toFixed(6), needed: needed.toFixed(6), short: (needed - usdcBal).toFixed(6) }, fundingInstructions: { network: "Base mainnet (Chain ID 8453)", tokenContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", sendTo: walletAddress, }, }); } const itemSummary = Array.isArray(items) ? items.map((i: any) => `${i.qty ?? 1}x ${i.name ?? i}`).join(", ") : null; const memo = [orderId && `Order#${orderId}`, itemSummary, deliveryNote].filter(Boolean).join(" | "); const result = await agent.payments.send({ to, amount, token, chain }); return JSON.stringify({ success: true, orderId: orderId ?? null, items: items ?? null, deliveryNote: deliveryNote ?? null, txHash: result.txHash, explorerUrl: result.explorerUrl ?? `https://basescan.org/tx/${result.txHash}`, receipt: result.receipt ?? null, memo, message: `Order payment of ${amount} ${token} confirmed on-chain.`, }); }, }, { name: "fluid_delegate_task", description: "Fund another agent, developer, or compute service to perform a task. " + "Use this when an AI agent needs to pay a specialist agent or service to do work — " + "e.g. 'Pay Devin to debug the staging build and open a PR', " + "'Fund the code review agent $10 USDC', 'Send $5 to the testing pipeline', " + "'Pay the security audit agent', 'Buy compute credits for the build runner'. " + "Sends USDC to another Fluid Wallet user by email, or to a wallet address. " + "Checks balance first. Returns receipt with BaseScan proof. " + "Requires 'pay' or 'agentpay' scope.", inputSchema: { type: "object", required: ["amount", "taskDescription"], properties: { toEmail: { type: "string", description: "Recipient Fluid Wallet email — use this if you know their email" }, toAddress: { type: "string", description: "Recipient 0x wallet address — use if you have the address" }, amount: { type: "string", description: "Amount in USDC to send, e.g. '10'" }, token: { type: "string", description: "Token: USDC | ETH (default: USDC)" }, chain: { type: "string", description: "Chain: base | ethereum (default: base)" }, taskDescription: { type: "string", description: "What the recipient is being paid to do, e.g. 'Debug staging build and open PR'" }, expectedOutput: { type: "string", description: "What output or deliverable you expect back" }, }, }, async run(args) { const { toEmail, toAddress, amount, token = "USDC", chain = "base", taskDescription, expectedOutput } = args as Record; if (!toEmail && !toAddress) { return JSON.stringify({ success: false, error: "Provide either toEmail or toAddress for the recipient" }); } // Balance check const balData = await agent.getBalance(chain) as any; const balances: Array<{ token: string; amount: string }> = balData?.balances ?? []; const walletAddress: string | null = balData?.walletAddress ?? null; const usdcBal = parseFloat(balances.find(b => b.token?.toUpperCase() === "USDC")?.amount ?? "0"); const needed = parseFloat(amount); if (token.toUpperCase() === "USDC" && usdcBal < needed) { return JSON.stringify({ success: false, error: "Insufficient USDC to fund this task", balance: { available: usdcBal.toFixed(6), needed: needed.toFixed(6) }, fundingInstructions: { sendTo: walletAddress, network: "Base mainnet (Chain ID 8453)" }, }); } let result: any; if (toEmail) { result = await agent.payments.agentPay({ toEmail, amount, token, memo: taskDescription, }); } else { result = await agent.payments.send({ to: toAddress, amount, token, chain }); } return JSON.stringify({ success: true, taskDescription, expectedOutput: expectedOutput ?? null, recipient: toEmail ?? toAddress, txHash: result.txHash, explorerUrl: result.explorerUrl ?? `https://basescan.org/tx/${result.txHash}`, receipt: result.receipt ?? null, message: `Funded ${taskDescription} with ${amount} ${token}. Receipt on-chain.`, }); }, }, { name: "fluid_research_pay", description: "Call multiple data sources or research APIs, automatically paying any that charge " + "via the FADP protocol. Use this when an agent is asked to research a topic using " + "paid data sources and compile the results — " + "e.g. 'Research the top 10 agentic fintech startups and build a comparison sheet', " + "'Gather market data from these five premium APIs', " + "'Pull company financials from paid data endpoints and summarise them'. " + "Each URL in the list is fetched; if any returns 402 + FADP payment header it is " + "paid automatically (up to maxPayPerSourceUsd per source). " + "Returns all responses aggregated plus a payment summary for any sources that charged. " + "Requires 'pay' scope for paid sources.", inputSchema: { type: "object", required: ["sources"], properties: { sources: { type: "array", description: "List of data source URLs to fetch. Each may be free or FADP-gated.", items: { type: "string" }, }, maxPayPerSourceUsd: { type: "number", description: "Max USDC to auto-pay per data source (default: 2). Refused if cost exceeds this.", }, query: { type: "string", description: "Optional research query context — included in the returned results for reference.", }, method: { type: "string", description: "HTTP method for all sources: GET | POST (default: GET)", }, }, }, async run(args) { const { sources, maxPayPerSourceUsd = 2, query, method = "GET" } = args as Record; if (!Array.isArray(sources) || sources.length === 0) { return JSON.stringify({ success: false, error: "sources must be a non-empty array of URLs" }); } const fadpFetch = agent.fadpFetch({ maxAutoPayUsd: Number(maxPayPerSourceUsd) }); const results: Array<{ url: string; status: number; data: unknown; paid: boolean; error?: string }> = []; let totalPaidSources = 0; for (const url of sources as string[]) { try { // First probe — see if it's free or paid const probe = await fetch(url, { method }); const wasFree = probe.status !== 402; let res: Response; if (wasFree) { res = probe; } else { res = await fadpFetch(url, { method }); totalPaidSources++; } const text = await res.text(); let data: unknown; try { data = JSON.parse(text); } catch { data = text; } results.push({ url, status: res.status, data, paid: !wasFree }); } catch (e: any) { results.push({ url, status: 0, data: null, paid: false, error: e.message ?? String(e) }); } } return JSON.stringify({ success: true, query: query ?? null, totalSources: sources.length, successfulSources: results.filter(r => r.status >= 200 && r.status < 300).length, paidSources: totalPaidSources, results, }); }, }, // ── Pauli Keys ──────────────────────────────────────────────────────── { name: "fluid_key_create", description: "Create a new scoped Pauli key (child agent key). " + "The new key's scopes must be a subset of the current agent key's scopes (child ⊆ parent rule). " + "Use this to spawn worker agents with limited permissions. " + "Requires the 'spawn' scope.", inputSchema: { type: "object", required: ["label", "scopes"], properties: { label: { type: "string", description: "Human-readable name, e.g. 'payment-worker-1'" }, scopes: { type: "array", description: "Permissions: ['read'], ['pay'], ['swap'], ['read','pay'], etc.", items: { type: "string" } }, spendLimit: { type: "string", description: "Max USD per transaction (e.g. '100')" }, dailyLimit: { type: "string", description: "Max USD per day (e.g. '500')" }, expiresIn: { type: "number", description: "Key lifetime in seconds (0 = no expiry)" }, }, }, async run(args) { return JSON.stringify(await agent.keys.create({ label: args.label as string, scopes: args.scopes as string[], spendLimit: args.spendLimit as string | undefined, dailyLimit: args.dailyLimit as string | undefined, expiresIn: args.expiresIn as number | undefined, })); }, }, { name: "fluid_key_list", description: "List all active Pauli keys (child agent keys) created under the current key. " + "Returns metadata including scopes, spend limits, last used time, and usage count.", inputSchema: { type: "object", properties: {} }, async run() { return JSON.stringify(await agent.keys.list()); }, }, { name: "fluid_key_revoke", description: "Revoke (immediately deactivate) a Pauli key by its keyId. " + "The key stops working instantly — any in-flight requests with it will fail. " + "Requires the 'spawn' scope.", inputSchema: { type: "object", required: ["keyId"], properties: { keyId: { type: "string", description: "The keyId to revoke, e.g. 'fwag_pay_a3f9c2...'" }, }, }, async run(args) { return JSON.stringify(await agent.keys.revoke(args.keyId as string)); }, }, ]; } // ─── OpenAI function calling ─────────────────────────────────────────────── // Pass directly to `tools` in openai.chat.completions.create() export interface OpenAITool { type: "function"; function: { name: string; description: string; parameters: object }; } export function toOpenAITools(agent: FluidAgent): OpenAITool[] { return getFluidTools(agent).map(t => ({ type: "function" as const, function: { name: t.name, description: t.description, parameters: t.inputSchema }, })); } export async function runOpenAIToolCall( agent: FluidAgent, toolCall: { function: { name: string; arguments: string } } ): Promise { const tool = getFluidTools(agent).find(t => t.name === toolCall.function.name); if (!tool) throw new Error(`Unknown Fluid tool: ${toolCall.function.name}`); return tool.run(JSON.parse(toolCall.function.arguments)); } // ─── Anthropic Claude tool_use ───────────────────────────────────────────── // Pass directly to `tools` in anthropic.messages.create() export interface AnthropicTool { name: string; description: string; input_schema: { type: "object"; properties: Record; required?: string[] }; } export function toAnthropicTools(agent: FluidAgent): AnthropicTool[] { return getFluidTools(agent).map(t => ({ name: t.name, description: t.description, input_schema: t.inputSchema as AnthropicTool["input_schema"], })); } export async function runAnthropicToolCall( agent: FluidAgent, block: { name: string; input: Record } ): Promise { const tool = getFluidTools(agent).find(t => t.name === block.name); if (!tool) throw new Error(`Unknown Fluid tool: ${block.name}`); return tool.run(block.input); } // ─── Vercel AI SDK ───────────────────────────────────────────────────────── // Compatible with generateText / streamText tools parameter. // Usage: import { jsonSchema } from "ai"; const tools = toVercelAITools(agent, jsonSchema); export function toVercelAITools( agent: FluidAgent, jsonSchemaHelper: (schema: object) => unknown ): Record) => Promise }> { const result: Record = {}; for (const t of getFluidTools(agent)) { result[t.name] = { description: t.description, parameters: jsonSchemaHelper(t.inputSchema), execute: (args: Record) => t.run(args), }; } return result; } // ─── LangChain ───────────────────────────────────────────────────────────── // Wrap each with: new DynamicStructuredTool(t) from @langchain/core/tools export interface LangChainToolDef { name: string; description: string; func: (args: Record) => Promise; } export function toLangChainTools(agent: FluidAgent): LangChainToolDef[] { return getFluidTools(agent).map(t => ({ name: t.name, description: t.description, func: (args) => t.run(args), })); }