diff --git a/typescript/examples/langchain-mainstreet-trust-gate/.env-local b/typescript/examples/langchain-mainstreet-trust-gate/.env-local new file mode 100644 index 000000000..8ef06a10e --- /dev/null +++ b/typescript/examples/langchain-mainstreet-trust-gate/.env-local @@ -0,0 +1,4 @@ +CDP_API_KEY_ID= +CDP_API_KEY_SECRET= +OPENAI_API_KEY= +MAINSTREET_MIN_SCORE=30 diff --git a/typescript/examples/langchain-mainstreet-trust-gate/README.md b/typescript/examples/langchain-mainstreet-trust-gate/README.md new file mode 100644 index 000000000..9186b05e8 --- /dev/null +++ b/typescript/examples/langchain-mainstreet-trust-gate/README.md @@ -0,0 +1,79 @@ +# MainStreet Trust-Gate Chatbot + +This example demonstrates a buyer agent that **refuses to pay any wallet scoring below a configurable MainStreet reputation threshold**. + +## What it does + +``` +user → "send 1 USDC to 0xAbc..." + ↓ +tool: mainstreet_trust_check + ├── fetch /api/agent/attestation/0xAbc + ├── verify EIP-712 signature against MainStreet operator (0xAC3c...) + ├── check freshness (<24h) + └── check score >= MIN_SCORE (default 30) + ↓ (only if ok) +tool: CDP erc20 transfer +``` + +If the wallet's MainStreet score is below threshold (or no attestation exists, or signature invalid), the agent refuses and explains why. + +## Why this matters + +CDP Bazaar lists 800+ AI agents on Base. Some are legitimate businesses settling thousands of USDC; some are throwaway wallets that drained LPs and disappeared. **Today AgentKit users can pay any of them.** A trust gate prevents users from paying low-rep wallets they wouldn't pay if they could see the score. + +MainStreet is an EIP-712 reputation oracle on Base. Free attestation fetch, free off-chain verify, onchain verifier contract live at [`0x7397adb9713934c36d22aa54b4dbbcd70263592b`](https://basescan.org/address/0x7397adb9713934c36d22aa54b4dbbcd70263592b). + +## Configuration + +```env +CDP_API_KEY_ID=... +CDP_API_KEY_SECRET=... +OPENAI_API_KEY=... +MAINSTREET_MIN_SCORE=30 # default 30/100; raise for stricter gate +``` + +## Run + +```bash +npm install +npm start +``` + +Try: + +``` +you: what's the MainStreet score for 0xAC3ca7c5d3cDD7702fd08F9C4C28dAA22296aDa9? +you: send 0.01 USDC to 0xAC3ca7c5d3cDD7702fd08F9C4C28dAA22296aDa9 +you: send 0.01 USDC to 0x0000000000000000000000000000000000000001 +``` + +The first known address will pass the gate; the second (no attestation) will be refused with `reason: "no attestation found"`. + +## How verification works + +MainStreet attestations follow EIP-712 with this schema: + +``` +Attestation( + string version, // "mainstreet-v1" + string subjectType, // "agent-onchain" + bytes32 subject, // sha256(toLowerCase(address)) + uint8 score, // 0-100 + uint64 timestamp, // ≤24h old + address operator, // 0xAC3ca7c5d3cDD7702fd08F9C4C28dAA22296aDa9 + uint64 nonce +) +``` + +Domain: `{ name: "MainStreet", version: "1", chainId: 8453 }`. + +For on-chain verification (smart contract gate), call the deployed `MainStreetVerifier.requireMinScore()` instead — see [avisradar.app/integrations.html](https://avisradar.app/integrations.html). + +## Resources + +- MainStreet oracle landing: +- Catalog of paid endpoints: +- Integration recipes for 10+ Base ecosystem platforms: +- npm package: [`@raskhaaa/mainstreet-oracle`](https://www.npmjs.com/package/@raskhaaa/mainstreet-oracle) (includes `/verifier` helpers) +- Open-source repo: diff --git a/typescript/examples/langchain-mainstreet-trust-gate/chatbot.ts b/typescript/examples/langchain-mainstreet-trust-gate/chatbot.ts new file mode 100644 index 000000000..28b53a0e8 --- /dev/null +++ b/typescript/examples/langchain-mainstreet-trust-gate/chatbot.ts @@ -0,0 +1,92 @@ +/** + * MainStreet Trust-Gate Chatbot — AgentKit example. + * + * Demonstrates a buyer agent that REFUSES to pay any wallet scoring below + * a configurable MainStreet threshold. The reputation lookup is free; the + * payment only happens when score >= MIN_SCORE. + * + * Pattern: + * user → "send 1 USDC to 0xAbc..." + * ↓ + * tool: mainstreet_score_check → fetch + verify EIP-712 attestation + * ↓ (score >= MIN_SCORE) + * tool: native CDP transfer + * + * MainStreet docs: https://avisradar.app/oracle.html + * Deployed verifier on Base: 0x7397adb9713934c36d22aa54b4dbbcd70263592b + */ +import * as dotenv from "dotenv"; +import { ChatOpenAI } from "@langchain/openai"; +import { HumanMessage } from "@langchain/core/messages"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { AgentKit, walletActionProvider, erc20ActionProvider, CdpEvmWalletProvider } from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { recoverTypedDataAddress } from "viem"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +dotenv.config(); + +const MIN_SCORE = parseInt(process.env.MAINSTREET_MIN_SCORE || "30", 10); +const MS_SIGNER = "0xAC3ca7c5d3cDD7702fd08F9C4C28dAA22296aDa9"; + +const mainstreetTrustCheck = tool( + async ({ address }: { address: string }) => { + const att = await fetch(`https://avisradar.app/api/agent/attestation/${address}`).then((r) => r.json()); + if (!att.payload) return JSON.stringify({ ok: false, reason: "no attestation found" }); + const recovered = await recoverTypedDataAddress({ + domain: att.eip712.domain, + types: att.eip712.types, + primaryType: "Attestation", + message: att.payload, + signature: att.signature, + }); + if (recovered.toLowerCase() !== MS_SIGNER.toLowerCase()) return JSON.stringify({ ok: false, reason: "signature invalid" }); + const ageSec = Math.floor(Date.now() / 1000) - Number(att.payload.timestamp); + if (ageSec > 86400) return JSON.stringify({ ok: false, reason: "attestation stale (>24h)" }); + if (att.payload.score < MIN_SCORE) + return JSON.stringify({ ok: false, reason: `score ${att.payload.score} < ${MIN_SCORE}` }); + return JSON.stringify({ ok: true, score: att.payload.score, tier: att.payload.score >= 60 ? "high" : "medium" }); + }, + { + name: "mainstreet_trust_check", + description: `Check MainStreet reputation BEFORE paying any wallet. Verifies an EIP-712 signed attestation. Returns {ok, score, tier} or {ok: false, reason}. Use this BEFORE every transfer/swap. Free.`, + schema: z.object({ address: z.string().describe("0x-prefixed wallet to check") }), + } +); + +async function main() { + const walletProvider = await CdpEvmWalletProvider.configureWithWallet({ + apiKeyId: process.env.CDP_API_KEY_ID!, + apiKeySecret: process.env.CDP_API_KEY_SECRET!, + networkId: "base-mainnet", + }); + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [walletActionProvider(), erc20ActionProvider()], + }); + const tools = [...(await getLangChainTools(agentkit)), mainstreetTrustCheck]; + const agent = createReactAgent({ + llm: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }), + tools, + checkpointer: new MemorySaver(), + messageModifier: `You are a careful onchain payment agent. NEVER send funds to an address without first calling mainstreet_trust_check. If the check returns ok:false, refuse the transfer and explain why.`, + }); + const config = { configurable: { thread_id: "mainstreet-trust-gate" } }; + console.log(`MainStreet Trust-Gate active. MIN_SCORE=${MIN_SCORE}. Type a request or "exit".`); + const readline = await import("node:readline/promises"); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + for (;;) { + const userInput = await rl.question("\nyou: "); + if (!userInput || userInput.trim().toLowerCase() === "exit") break; + const stream = await agent.stream({ messages: [new HumanMessage(userInput)] }, config); + for await (const chunk of stream) { + if ("agent" in chunk) console.log("\nagent:", chunk.agent.messages[chunk.agent.messages.length - 1].content); + else if ("tools" in chunk) console.log("\ntool:", chunk.tools.messages[0].content); + } + } + rl.close(); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/typescript/examples/langchain-mainstreet-trust-gate/package.json b/typescript/examples/langchain-mainstreet-trust-gate/package.json new file mode 100644 index 000000000..1873e34f8 --- /dev/null +++ b/typescript/examples/langchain-mainstreet-trust-gate/package.json @@ -0,0 +1,27 @@ +{ + "name": "@coinbase/agentkit-langchain-mainstreet-trust-gate-example", + "description": "AgentKit example — gate x402 payments by MainStreet reputation score before sending USDC", + "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" + }, + "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", + "viem": "^2.0.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.0", + "tsx": "^4.7.1" + } +} diff --git a/typescript/examples/langchain-mainstreet-trust-gate/tsconfig.json b/typescript/examples/langchain-mainstreet-trust-gate/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/langchain-mainstreet-trust-gate/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +}