diff --git a/README.md b/README.md index 33703c4c1..806b17073 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ agentkit/ │ ├── langchain-cdp-chatbot/ │ ├── langchain-cdp-smart-wallet-chatbot/ │ ├── langchain-farcaster-chatbot/ +│ ├── langchain-krewe-inference-chatbot/ │ ├── langchain-legacy-cdp-chatbot/ │ ├── langchain-privy-chatbot/ │ ├── langchain-solana-chatbot/ diff --git a/typescript/examples/langchain-krewe-inference-chatbot/.env-local b/typescript/examples/langchain-krewe-inference-chatbot/.env-local new file mode 100644 index 000000000..b74bc669c --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/.env-local @@ -0,0 +1,24 @@ +# Required +OPENAI_API_KEY= +CDP_API_KEY_ID= +CDP_API_KEY_SECRET= +CDP_WALLET_SECRET= + +# Optional — defaults to base-mainnet for live krewe inference. Set to +# base-sepolia if you've pointed krewe at a sepolia orchestrator. +NETWORK_ID=base-mainnet + +# Optional — override the krewe orchestrator URL (e.g. for self-hosted +# orchestrators or staging environments). Defaults to the production +# Railway orchestrator. +KREWE_PREDICT_URL=https://krewe-orchestrator-production.up.railway.app/v2/predict + +# Optional — hard ceiling on how much USDC the agent will pay per call. +# Defaults to $0.10. Pricing today: $0.005 text.structure, $0.01 text.embed, +# $0.02 web.scrape, $0.05 text.complete. +KREWE_MAX_PAYMENT_USDC=0.10 + +# Optional account-management knobs (inherited from the CDP template) +IDEMPOTENCY_KEY= +ADDRESS= +RPC_URL= diff --git a/typescript/examples/langchain-krewe-inference-chatbot/.eslintrc.json b/typescript/examples/langchain-krewe-inference-chatbot/.eslintrc.json new file mode 100644 index 000000000..91571ba7a --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["../../.eslintrc.base.json"] +} diff --git a/typescript/examples/langchain-krewe-inference-chatbot/.prettierignore b/typescript/examples/langchain-krewe-inference-chatbot/.prettierignore new file mode 100644 index 000000000..20de531f4 --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +coverage/ +.github/ +src/client +**/**/*.json +*.md diff --git a/typescript/examples/langchain-krewe-inference-chatbot/.prettierrc b/typescript/examples/langchain-krewe-inference-chatbot/.prettierrc new file mode 100644 index 000000000..ffb416b74 --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/examples/langchain-krewe-inference-chatbot/README.md b/typescript/examples/langchain-krewe-inference-chatbot/README.md new file mode 100644 index 000000000..f689b3357 --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/README.md @@ -0,0 +1,98 @@ +# krewe Inference + AgentKit + LangChain — Chatbot example + +This example shows how a LangChain agent can call **[krewe](https://www.krewe.world)** — a decentralized AI inference network on Base mainnet — using AgentKit's built-in `x402ActionProvider`. Each inference call is paid per-request in USDC via the [x402 protocol](https://x402.org); no API keys or pre-paid quotas required. + +The agent has a CDP-managed Base wallet and four inference tools through krewe: + +| `kind` | Cost | What it does | +|---|---|---| +| `text.structure` | $0.005 | Regex/JSON extraction from unstructured text | +| `text.embed` | $0.01 | Sentence embeddings via a small quantized model | +| `web.scrape` | $0.02 | Fetch + clean a URL payload | +| `text.complete` | $0.05 | Small-language-model completion (greedy decode, server-pinned model) | + +Every USDC paid through this example flows directly back to the miners that answered the job — krewe's orchestrator sweeps payments hourly, swaps them for `$KREW` on Uniswap V4, and deposits the proceeds into the registry's reward pool. + +## Prompts to try + +- `"Extract every email and date from this text: 'RSVP to hi@krewe.world by 2026-05-17 or call us 2026-05-20.'"` +- `"Embed this sentence: 'Decentralized AI inference on Base'."` +- `"Scrape https://www.krewe.world and tell me what the project does."` +- `"Use krewe's text.complete to write a one-sentence summary of x402."` + +## Prerequisites + +### Node version + +Node.js 20 or higher. Check with `node --version`. If you need an upgrade, [`nvm`](https://github.com/nvm-sh/nvm) is the easiest path: + +```bash +nvm install node +``` + +### Keys + +- **CDP API key** — https://portal.cdp.coinbase.com/access/api +- **CDP wallet secret** — https://portal.cdp.coinbase.com/products/wallet-api +- **OpenAI API key** — https://platform.openai.com/docs/quickstart#create-and-export-an-api-key + +Rename `.env-local` to `.env` and fill in: + +```bash +OPENAI_API_KEY=... +CDP_API_KEY_ID=... +CDP_API_KEY_SECRET=... +CDP_WALLET_SECRET=... + +# Defaults shown — override if you're pointing at a self-hosted orchestrator +NETWORK_ID=base-mainnet +KREWE_PREDICT_URL=https://krewe-orchestrator-production.up.railway.app/v2/predict +KREWE_MAX_PAYMENT_USDC=0.10 +``` + +### Funding the agent + +The agent's CDP wallet needs **USDC on Base mainnet** to pay for inference. The cheapest job (`text.structure`) costs $0.005, so $1 in USDC funds 200 calls. Send USDC to the wallet address printed at startup, or use the CDP funding faucet on Base sepolia and re-point `NETWORK_ID` if you've stood up a sepolia orchestrator. + +> Note: krewe's *production* orchestrator runs only on Base mainnet today. The `NETWORK_ID=base-sepolia` path works against a self-hosted orchestrator. + +## Running the example + +From the repository root: + +```bash +pnpm install +pnpm build +``` + +Then from this directory: + +```bash +pnpm start +``` + +You'll be prompted for **chat** (interactive prompts) or **auto** (autonomous loop that fires a creative krewe job every 30s). + +## How the x402 handshake works under the hood + +The flow is invisible to your agent code, but worth knowing: + +1. Agent calls `https://krewe-orchestrator-production.up.railway.app/v2/predict` with a job body. +2. Orchestrator returns `402 Payment Required` with a signed EIP-3009 challenge specifying the USDC `value`, `to`, `nonce`, and validity window. +3. AgentKit's `x402ActionProvider` (via `@x402/fetch`) signs the challenge with the CDP wallet. +4. Orchestrator verifies the signature, submits `transferWithAuthorization` on the USDC contract, waits for confirmation, then runs the inference job through krewe's miner network. +5. 2-of-3 byte-equal miner consensus produces the output, which is returned along with the on-chain `paymentTxHash`. + +Full protocol writeup: [`docs/x402.md`](https://github.com/krewe-AI/krewe/blob/main/docs/x402.md) in the krewe repo. + +## Want a self-hosted orchestrator? + +The orchestrator is open-source and small (~2k LOC of TypeScript). See [`SETUP.md`](https://github.com/krewe-AI/krewe/blob/main/SETUP.md) in the krewe repo for the dev path, and [`docs/architecture.md`](https://github.com/krewe-AI/krewe/blob/main/docs/architecture.md) for the network model. Point `KREWE_PREDICT_URL` at your instance and this example works against it unchanged. + +## Links + +- krewe site: https://www.krewe.world +- krewe repo: https://github.com/krewe-AI/krewe +- `$KREW` on Basescan: https://basescan.org/address/0xc411E6deE1F70F57F08714D3bd54b4F36f904B07 +- x402 spec: https://x402.org +- AgentKit docs: https://docs.cdp.coinbase.com/agentkit/docs/welcome diff --git a/typescript/examples/langchain-krewe-inference-chatbot/chatbot.ts b/typescript/examples/langchain-krewe-inference-chatbot/chatbot.ts new file mode 100644 index 000000000..863d2e128 --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/chatbot.ts @@ -0,0 +1,273 @@ +import { + AgentKit, + CdpEvmWalletProvider, + walletActionProvider, + cdpApiActionProvider, + cdpEvmWalletActionProvider, + erc20ActionProvider, + x402ActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { HumanMessage } from "@langchain/core/messages"; +import { MemorySaver } from "@langchain/langgraph"; +import { createAgent } from "langchain"; +import { ChatOpenAI } from "@langchain/openai"; +import * as dotenv from "dotenv"; +import * as readline from "readline"; + +dotenv.config(); + +const DEFAULT_KREWE_PREDICT_URL = "https://krewe-orchestrator-production.up.railway.app/v2/predict"; +const DEFAULT_MAX_PAYMENT_USDC = 0.1; + +/** + * Validates that required environment variables are set. + * + * @throws {Error} - If required environment variables are missing + * @returns {void} + */ +function validateEnvironment(): void { + const requiredVars = [ + "OPENAI_API_KEY", + "CDP_API_KEY_ID", + "CDP_API_KEY_SECRET", + "CDP_WALLET_SECRET", + ]; + const missing = requiredVars.filter(name => !process.env[name]); + if (missing.length > 0) { + console.error("Error: Required environment variables are not set"); + missing.forEach(name => console.error(`${name}=your_${name.toLowerCase()}_here`)); + process.exit(1); + } + if (!process.env.NETWORK_ID) { + console.warn("Warning: NETWORK_ID not set, defaulting to base-mainnet (krewe is mainnet-only today)"); + } +} + +validateEnvironment(); + +/** + * Initialize the agent with AgentKit + the krewe x402 inference service. + * + * @returns Agent executor and config + */ +async function initializeAgent() { + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + const networkId = process.env.NETWORK_ID || "base-mainnet"; + + const walletProvider = await CdpEvmWalletProvider.configureWithWallet({ + apiKeyId: process.env.CDP_API_KEY_ID, + apiKeySecret: process.env.CDP_API_KEY_SECRET, + walletSecret: process.env.CDP_WALLET_SECRET, + idempotencyKey: process.env.IDEMPOTENCY_KEY, + address: process.env.ADDRESS as `0x${string}` | undefined, + networkId, + rpcUrl: process.env.RPC_URL, + }); + + // krewe is an x402-paywalled inference network on Base mainnet. The + // built-in x402ActionProvider does the entire handshake: 402 challenge, + // EIP-3009 signature, on-chain USDC settlement, and the actual call. + // + // Pricing per call (USDC base units, 6 decimals): + // $0.005 text.structure · $0.01 text.embed + // $0.02 web.scrape · $0.05 text.complete + // + // Every USDC paid here is swept hourly by krewe's buyback service, swapped + // for $KREW on Uniswap V4, and deposited back into the registry's reward + // pool — so each call directly funds the miners that answered it. + const kreweUrl = process.env.KREWE_PREDICT_URL ?? DEFAULT_KREWE_PREDICT_URL; + const maxPaymentUsdc = Number(process.env.KREWE_MAX_PAYMENT_USDC ?? DEFAULT_MAX_PAYMENT_USDC); + + const actionProviders = [ + walletActionProvider(), + cdpApiActionProvider(), + cdpEvmWalletActionProvider(), + erc20ActionProvider(), + x402ActionProvider({ + registeredServices: [kreweUrl], + allowDynamicServiceRegistration: false, + maxPaymentUsdc, + registeredFacilitators: {}, + }), + ]; + + const agentkit = await AgentKit.from({ walletProvider, actionProviders }); + const tools = await getLangChainTools(agentkit); + + const memory = new MemorySaver(); + const agentConfig = { configurable: { thread_id: "krewe Inference Chatbot Example" } }; + + const agent = createAgent({ + model: llm, + tools, + checkpointer: memory, + systemPrompt: ` +You are an onchain agent with access to: + • A CDP EVM wallet on Base. + • The krewe inference network at ${kreweUrl}, accessed via the x402 + action provider. krewe is a decentralized AI inference network — your + wallet pays USDC per call (cap: $${maxPaymentUsdc}). The supported job + kinds are text.structure (regex/JSON extraction, $0.005), text.embed + (sentence embeddings, $0.01), web.scrape (clean HTML fetch, $0.02), + and text.complete (small-LM completion, $0.05). + +When a user asks for structured extraction, embeddings, a clean scrape, +or a quick small-model completion, prefer routing through krewe via the +x402 service — it pays krewe miners and gets a 2-of-3 consensus output. + +Always include the relevant body when calling the krewe endpoint: + { "kind": "text.structure" | "text.embed" | "web.scrape" | "text.complete", + "payload": } + +If a call returns 402 'payment-required', the x402 provider handles the +signature + settlement automatically — just retry once via the same tool. + +Be concise. If you can't do something with your current tools, say so. +`, + }); + + return { agent, config: agentConfig }; +} + +/** + * Run the agent interactively based on user input. + * + * @param agent - The agent executor + * @param config - Agent configuration + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runChatMode(agent: any, config: any) { + console.log("Starting chat mode... Type 'exit' to end."); + console.log("Try: \"Extract emails and dates from this text: Email hi@krewe.world by 2026-05-17\""); + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const question = (prompt: string): Promise => + new Promise(resolve => rl.question(prompt, resolve)); + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const userInput = await question("\nPrompt: "); + console.log("-------------------"); + if (userInput.toLowerCase() === "exit") break; + + const stream = await agent.stream({ messages: [new HumanMessage(userInput)] }, config); + + for await (const chunk of stream) { + if ("model_request" in chunk) { + const response = chunk.model_request.messages[0].content; + if (response !== "") console.log("\n Response: " + response); + } + if ("tools" in chunk) { + for (const tool of chunk.tools.messages) { + console.log("Tool " + tool.name + ": " + tool.content); + } + } + } + console.log("-------------------"); + } + } catch (error) { + if (error instanceof Error) console.error("Error:", error.message); + process.exit(1); + } finally { + rl.close(); + } +} + +/** + * Run the agent autonomously — picks a creative krewe inference job each + * tick to exercise the network. + * + * @param agent - The agent executor + * @param config - Agent configuration + * @param interval - Time interval between actions in seconds + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runAutonomousMode(agent: any, config: any, interval = 30) { + console.log("Starting autonomous mode..."); + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const thought = + "Pick an interesting short snippet of text or a public URL and route a krewe job through " + + "the x402 service to do something useful with it (extract structured data, embed, summarize, " + + "or scrape). Tell me the kind you chose, the payment cost, and the resulting output."; + + const stream = await agent.stream({ messages: [new HumanMessage(thought)] }, config); + + for await (const chunk of stream) { + if ("model_request" in chunk) { + const response = chunk.model_request.messages[0].content; + if (response !== "") console.log("\n Response: " + response); + } + if ("tools" in chunk) { + for (const tool of chunk.tools.messages) { + console.log("Tool " + tool.name + ": " + tool.content); + } + } + } + console.log("-------------------"); + + await new Promise(resolve => setTimeout(resolve, interval * 1000)); + } catch (error) { + if (error instanceof Error) console.error("Error:", error.message); + process.exit(1); + } + } +} + +/** + * Choose whether to run in autonomous or chat mode based on user input. + * + * @returns Selected mode + */ +async function chooseMode(): Promise<"chat" | "auto"> { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const question = (prompt: string): Promise => + new Promise(resolve => rl.question(prompt, resolve)); + + // eslint-disable-next-line no-constant-condition + while (true) { + console.log("\nAvailable modes:"); + console.log("1. chat - Interactive chat mode"); + console.log("2. auto - Autonomous action mode"); + + const choice = (await question("\nChoose a mode (enter number or name): ")) + .toLowerCase() + .trim(); + if (choice === "1" || choice === "chat") { + rl.close(); + return "chat"; + } else if (choice === "2" || choice === "auto") { + rl.close(); + return "auto"; + } + console.log("Invalid choice. Please try again."); + } +} + +/** + * Start the chatbot agent. + */ +async function main() { + try { + const { agent, config } = await initializeAgent(); + const mode = await chooseMode(); + if (mode === "chat") await runChatMode(agent, config); + else await runAutonomousMode(agent, config); + } catch (error) { + if (error instanceof Error) console.error("Error:", error.message); + process.exit(1); + } +} + +if (require.main === module) { + console.log("Starting krewe-inference agent..."); + main().catch(error => { + console.error("Fatal error:", error); + process.exit(1); + }); +} diff --git a/typescript/examples/langchain-krewe-inference-chatbot/package.json b/typescript/examples/langchain-krewe-inference-chatbot/package.json new file mode 100644 index 000000000..61608ae23 --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/package.json @@ -0,0 +1,30 @@ +{ + "name": "@coinbase/krewe-inference-langchain-chatbot-example", + "description": "AgentKit example: a LangChain chatbot that uses krewe's x402-paywalled inference network (text.structure, text.embed, web.scrape, text.complete) on Base mainnet.", + "version": "1.0.0", + "private": true, + "author": "Coinbase Inc.", + "license": "Apache-2.0", + "scripts": { + "start": "NODE_OPTIONS='--no-warnings' tsx ./chatbot.ts", + "dev": "nodemon ./chatbot.ts", + "lint": "eslint -c .eslintrc.json *.ts", + "lint:fix": "eslint -c .eslintrc.json *.ts --fix", + "format": "prettier --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"" + }, + "dependencies": { + "@coinbase/agentkit": "workspace:*", + "@coinbase/agentkit-langchain": "workspace:*", + "@langchain/core": "^1.1.0", + "@langchain/langgraph": "^1.2.0", + "@langchain/openai": "^1.2.0", + "dotenv": "^16.4.5", + "langchain": "^1.1.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.0", + "tsx": "^4.7.1" + } +} diff --git a/typescript/examples/langchain-krewe-inference-chatbot/tsconfig.json b/typescript/examples/langchain-krewe-inference-chatbot/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/langchain-krewe-inference-chatbot/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +}