diff --git a/README.md b/README.md
index 33703c4c1..6b8f986d1 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,8 @@ agentkit/
│ │ └── wallet-providers/
│ │ ├── cdp/
│ │ ├── privy/
-│ │ └── viem/
+│ │ ├── viem/
+│ │ └── waap/
│ ├── create-onchain-agent/
│ ├── framework-extensions/
│ │ ├── langchain/
@@ -158,10 +159,12 @@ agentkit/
│ ├── langchain-privy-chatbot/
│ ├── langchain-solana-chatbot/
│ ├── langchain-twitter-chatbot/
+│ ├── langchain-waap-chatbot/
│ ├── langchain-xmtp-chatbot/
│ ├── langchain-zerodev-chatbot/
│ ├── model-context-protocol-smart-wallet-server/
│ └── vercel-ai-sdk-smart-wallet-chatbot/
+│ └── vercel-ai-sdk-waap-chatbot/
├── python/
│ ├── coinbase-agentkit/
│ │ └── coinbase_agentkit/
@@ -279,6 +282,7 @@ AgentKit is proud to have support for the following protocols, frameworks, walle
+
### Protocols
diff --git a/assets/wallets/waap.svg b/assets/wallets/waap.svg
new file mode 100644
index 000000000..ff36c0191
--- /dev/null
+++ b/assets/wallets/waap.svg
@@ -0,0 +1,56 @@
+
diff --git a/typescript/.changeset/add-waap-wallet-provider.md b/typescript/.changeset/add-waap-wallet-provider.md
new file mode 100644
index 000000000..48564fff7
--- /dev/null
+++ b/typescript/.changeset/add-waap-wallet-provider.md
@@ -0,0 +1,5 @@
+---
+"@coinbase/agentkit": patch
+---
+
+Added WaaP wallet provider with 2PC split-custody key management
diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md
index 37b14207f..ea2c3dee3 100644
--- a/typescript/agentkit/README.md
+++ b/typescript/agentkit/README.md
@@ -50,6 +50,7 @@ AgentKit is a framework for easily enabling AI agents to take actions onchain. I
- [Configuring from CdpWalletProvider](#configuring-from-cdpwalletprovider)
- [Configuring from PrivyWalletProvider](#configuring-from-privywalletprovider)
- [Configuring from ViemWalletProvider](#configuring-from-viemwalletprovider)
+ - [WaapWalletProvider](#waapwalletprovider)
- [SVM Wallet Providers](#svm-wallet-providers)
- [CdpV2SolanaWalletProvider](#cdpv2solanawalletprovider)
- [Basic Configuration](#basic-configuration-2)
@@ -981,6 +982,7 @@ EVM:
- [ViemWalletProvider](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/wallet-providers/viemWalletProvider.ts)
- [PrivyWalletProvider](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/wallet-providers/privyWalletProvider.ts)
- [ZeroDevWalletProvider](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/wallet-providers/zeroDevWalletProvider.ts)
+- [WaapWalletProvider](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/wallet-providers/waapWalletProvider.ts)
### CdpEvmWalletProvider
@@ -1417,6 +1419,23 @@ const walletProvider = await ZeroDevWalletProvider.configureWithWallet({
});
```
+### WaapWalletProvider
+
+The `WaapWalletProvider` is an EVM wallet provider that uses the `waap-cli` binary. WaaP (Wallet as a Protocol) manages your private keys securely using two-party computation on the server-side, meaning that raw private keys never hit your local environment. The provider shells out to the `waap-cli` executable for all signing operations.
+
+```typescript
+import { WaapWalletProvider } from "@coinbase/agentkit";
+
+// Configures the wallet synchronously and logs in the CLI.
+// Requires waap-cli to be installed and available in the local PATH.
+const walletProvider = WaapWalletProvider.configureWithWallet({
+ email: "your_email@example.com", // Optional, for auto-login
+ password: "password", // Optional, for auto-login
+ chainId: "11155111", // e.g., Ethereum Sepolia (11155111)
+ rpcUrl: "https://ethereum-sepolia-rpc.publicnode.com", // Optional overridable RPC node URL
+});
+```
+
## SVM Wallet Providers
Wallet providers give an agent access to a wallet. AgentKit currently supports the following wallet providers:
diff --git a/typescript/agentkit/src/wallet-providers/index.ts b/typescript/agentkit/src/wallet-providers/index.ts
index a24b3aa77..9bc723609 100644
--- a/typescript/agentkit/src/wallet-providers/index.ts
+++ b/typescript/agentkit/src/wallet-providers/index.ts
@@ -12,3 +12,4 @@ export * from "./privyEvmWalletProvider";
export * from "./privySvmWalletProvider";
export * from "./privyEvmDelegatedEmbeddedWalletProvider";
export * from "./zeroDevWalletProvider";
+export * from "./waapWalletProvider";
diff --git a/typescript/agentkit/src/wallet-providers/waapWalletProvider.integration.test.ts b/typescript/agentkit/src/wallet-providers/waapWalletProvider.integration.test.ts
new file mode 100644
index 000000000..12e78073f
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/waapWalletProvider.integration.test.ts
@@ -0,0 +1,230 @@
+/**
+ * Integration tests for WaapWalletProvider.
+ *
+ * These tests require a real waap-cli installation and valid credentials.
+ * They are skipped by default. To run them, set the following environment
+ * variables and remove the `.skip` from each `describe` block:
+ *
+ * WAAP_CLI_PATH - Path to the waap-cli binary (default: "waap-cli")
+ * WAAP_EMAIL - WaaP account email
+ * WAAP_PASSWORD - WaaP account password
+ * WAAP_CHAIN_ID - EVM chain ID (default: "84532" for Base Sepolia)
+ * WAAP_RPC_URL - RPC URL for the chain (optional)
+ *
+ * Run:
+ * WAAP_EMAIL=you@example.com WAAP_PASSWORD=secret
+ * npx jest --testMatch "**\/*.integration.test.ts" --no-cache
+ */
+
+import { WaapWalletProvider, WaapWalletProviderConfig } from "./waapWalletProvider";
+
+const WAAP_CLI_PATH = process.env.WAAP_CLI_PATH ?? "waap-cli";
+const WAAP_EMAIL = process.env.WAAP_EMAIL;
+const WAAP_PASSWORD = process.env.WAAP_PASSWORD;
+const WAAP_CHAIN_ID = process.env.WAAP_CHAIN_ID ?? "84532";
+const WAAP_RPC_URL = process.env.WAAP_RPC_URL;
+
+const canRun = Boolean(WAAP_EMAIL && WAAP_PASSWORD);
+
+const describeIntegration = canRun ? describe : describe.skip;
+
+describeIntegration("WaapWalletProvider integration", () => {
+ let provider: WaapWalletProvider;
+
+ const config: WaapWalletProviderConfig = {
+ cliPath: WAAP_CLI_PATH,
+ chainId: WAAP_CHAIN_ID,
+ rpcUrl: WAAP_RPC_URL,
+ email: WAAP_EMAIL,
+ password: WAAP_PASSWORD,
+ };
+
+ // =========================================================
+ // authentication & setup
+ // =========================================================
+
+ describe("authentication & setup", () => {
+ it("should log in and create a provider via configureWithWallet", () => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ expect(provider).toBeInstanceOf(WaapWalletProvider);
+ });
+ });
+
+ // =========================================================
+ // wallet identity
+ // =========================================================
+
+ describe("wallet identity", () => {
+ beforeAll(() => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ });
+
+ it("should return a valid EVM address from getAddress", () => {
+ const address = provider.getAddress();
+ expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ });
+
+ it("should return the correct network", () => {
+ const network = provider.getNetwork();
+ expect(network.protocolFamily).toBe("evm");
+ expect(network.chainId).toBe(WAAP_CHAIN_ID);
+ });
+
+ it("should return the provider name", () => {
+ expect(provider.getName()).toBe("waap_wallet_provider");
+ });
+ });
+
+ // =========================================================
+ // balance
+ // =========================================================
+
+ describe("balance", () => {
+ beforeAll(() => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ });
+
+ it("should fetch balance (>= 0)", async () => {
+ const balance = await provider.getBalance();
+ expect(balance).toBeGreaterThanOrEqual(BigInt(0));
+ });
+ });
+
+ // =========================================================
+ // signing operations
+ // =========================================================
+
+ describe("signing", () => {
+ beforeAll(() => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ });
+
+ it("should sign a message and return a valid signature", async () => {
+ const signature = await provider.signMessage("Hello from AgentKit integration test");
+ expect(signature).toMatch(/^0x[0-9a-fA-F]+$/);
+ // ECDSA signature is 65 bytes = 130 hex chars + "0x" prefix
+ expect(signature.length).toBe(132);
+ });
+
+ it("should sign typed data (EIP-712)", async () => {
+ const typedData = {
+ domain: {
+ name: "AgentKit Integration Test",
+ version: "1",
+ chainId: Number(WAAP_CHAIN_ID),
+ },
+ types: {
+ Greeting: [{ name: "text", type: "string" }],
+ },
+ primaryType: "Greeting",
+ message: { text: "Hello" },
+ };
+
+ const signature = await provider.signTypedData(typedData);
+ expect(signature).toMatch(/^0x[0-9a-fA-F]+$/);
+ expect(signature.length).toBe(132);
+ });
+
+ it("should sign a raw hash", async () => {
+ const hash =
+ "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as `0x${string}`;
+ const signature = await provider.sign(hash);
+ expect(signature).toMatch(/^0x[0-9a-fA-F]+$/);
+ });
+ });
+
+ // =========================================================
+ // transaction flow (testnet only)
+ // =========================================================
+
+ describe("transaction flow", () => {
+ beforeAll(() => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ });
+
+ it("should sign a transaction without broadcasting", async () => {
+ const address = provider.getAddress() as `0x${string}`;
+ const signedTx = await provider.signTransaction({
+ to: address, // self-transfer
+ value: BigInt(0),
+ });
+ expect(signedTx).toMatch(/^0x[0-9a-fA-F]+$/);
+ });
+
+ // This test sends a real 0-value transaction on testnet.
+ // It requires the wallet to have gas funds on the configured chain.
+ it(
+ "should send a 0-value self-transfer and get a receipt",
+ async () => {
+ const address = provider.getAddress() as `0x${string}`;
+ const balance = await provider.getBalance();
+
+ // Skip if wallet has no gas
+ if (balance === BigInt(0)) {
+ console.warn("Skipping send-tx test: wallet has no balance for gas.");
+ return;
+ }
+
+ const txHash = await provider.sendTransaction({
+ to: address,
+ value: BigInt(0),
+ });
+ expect(txHash).toMatch(/^0x[0-9a-fA-F]{64}$/);
+
+ const receipt = await provider.waitForTransactionReceipt(txHash);
+ expect(receipt).toBeDefined();
+ expect(receipt.transactionHash).toBe(txHash);
+ },
+ 60_000,
+ );
+ });
+
+ // =========================================================
+ // multi-agent isolation
+ // =========================================================
+
+ describe("multi-agent isolation via + email notation", () => {
+ it("should create a separate wallet for a + alias email", () => {
+ if (!WAAP_EMAIL) return;
+ const [localPart, domain] = WAAP_EMAIL.split("@");
+ const agentEmail = `${localPart}+agent-integration-test@${domain}`;
+
+ try {
+ const agentProvider = WaapWalletProvider.configureWithWallet({
+ ...config,
+ email: agentEmail,
+ });
+
+ const mainAddress = provider.getAddress();
+ const agentAddress = agentProvider.getAddress();
+
+ // Each + alias gets its own wallet, so addresses should differ
+ expect(agentAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ expect(agentAddress).not.toBe(mainAddress);
+ } catch (error) {
+ // Skip if the + alias account is not registered on the server
+ const msg = (error as Error).message || "";
+ if (msg.includes("401") || msg.includes("Invalid email")) {
+ console.warn("Skipping: + alias account not registered on server");
+ return;
+ }
+ throw error;
+ }
+ });
+ });
+
+ // =========================================================
+ // contract reads
+ // =========================================================
+
+ describe("contract reads", () => {
+ beforeAll(() => {
+ provider = WaapWalletProvider.configureWithWallet(config);
+ });
+
+ it("should return a PublicClient for read-only operations", () => {
+ const client = provider.getPublicClient();
+ expect(client).toBeDefined();
+ });
+ });
+});
diff --git a/typescript/agentkit/src/wallet-providers/waapWalletProvider.test.ts b/typescript/agentkit/src/wallet-providers/waapWalletProvider.test.ts
new file mode 100644
index 000000000..66bf183d5
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/waapWalletProvider.test.ts
@@ -0,0 +1,613 @@
+import { WaapWalletProvider, WaapWalletProviderConfig, WaapWalletExport } from "./waapWalletProvider";
+import * as child_process from "child_process";
+import { ReadContractParameters, Abi } from "viem";
+
+// =========================================================
+// global mocks
+// =========================================================
+
+global.fetch = jest.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ } as Response),
+);
+
+jest.mock("../analytics", () => ({
+ sendAnalyticsEvent: jest.fn().mockImplementation(() => Promise.resolve()),
+}));
+
+jest.mock("child_process", () => ({
+ execFileSync: jest.fn(),
+ execFile: jest.fn(),
+}));
+
+// Intercept promisify so execFileAsync uses our execFile mock with callback semantics.
+jest.mock("util", () => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const cp = require("child_process");
+ return {
+ ...jest.requireActual("util"),
+ promisify: (fn: unknown) => {
+ if (fn === cp.execFile) {
+ return (...args: unknown[]) =>
+ new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
+ (cp.execFile as jest.Mock)(
+ ...args,
+ (err: Error | null, stdout: string, stderr: string) => {
+ if (err) reject(err);
+ else resolve({ stdout, stderr });
+ },
+ );
+ });
+ }
+ return jest.requireActual("util").promisify(fn);
+ },
+ };
+});
+
+const mockPublicClient = {
+ getBalance: jest.fn(),
+ waitForTransactionReceipt: jest.fn(),
+ readContract: jest.fn(),
+};
+
+jest.mock("viem", () => {
+ const actual = jest.requireActual("viem");
+ return {
+ ...actual,
+ createPublicClient: jest.fn(() => mockPublicClient),
+ };
+});
+
+jest.mock("../network/network", () => ({
+ CHAIN_ID_TO_NETWORK_ID: {
+ 8453: "base-mainnet",
+ 84532: "base-sepolia",
+ 1: "ethereum-mainnet",
+ },
+ getChain: jest.fn().mockImplementation((chainId: string) => {
+ if (chainId === "999999") return undefined;
+ return {
+ id: Number(chainId),
+ name: "Base",
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
+ rpcUrls: { default: { http: ["https://mainnet.base.org"] } },
+ };
+ }),
+}));
+
+// =========================================================
+// consts
+// =========================================================
+
+const MOCK_ADDRESS = "0x6186E6CeD896981DDe6Da33830E697be900c95f5";
+const MOCK_SIGNATURE =
+ "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab1c";
+const MOCK_TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
+const MOCK_SIGNED_TX =
+ "0x02f87083210580843b9aca00850684ee180082520894dead000000000000000000000000000000000000872386f26fc1000080c0";
+
+const DEFAULT_CONFIG: WaapWalletProviderConfig = {
+ chainId: "8453",
+ rpcUrl: "https://mainnet.base.org",
+ cliPath: "/usr/local/bin/waap-cli",
+};
+
+const mockExecFileSync = child_process.execFileSync as jest.MockedFunction<
+ typeof child_process.execFileSync
+>;
+// Cast as jest.Mock (not MockedFunction) to avoid ChildProcess return-type errors in mockImplementation
+const mockExecFile = child_process.execFile as unknown as jest.Mock;
+
+/**
+ * Sets the return value for both sync and async CLI mocks simultaneously.
+ * Use this in tests to control what waap-cli "outputs".
+ */
+function mockCliOutput(output: string) {
+ mockExecFileSync.mockReturnValue(output);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockExecFile.mockImplementation((...args: any[]) => {
+ const cb = args[args.length - 1];
+ if (typeof cb === "function") cb(null, output, "");
+ });
+}
+
+// =========================================================
+// tests
+// =========================================================
+
+describe("WaapWalletProvider", () => {
+ let provider: WaapWalletProvider;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ const whoamiOutput = `🔑 Fetching encrypted keyshare ...\n 🔓 Decrypting keyshare ...\n ✅ Keyshare ready\nWallet address: ${MOCK_ADDRESS}`;
+ mockCliOutput(whoamiOutput);
+ mockPublicClient.getBalance.mockResolvedValue(BigInt("1000000000000000000"));
+ mockPublicClient.waitForTransactionReceipt.mockResolvedValue({
+ transactionHash: MOCK_TX_HASH,
+ });
+ mockPublicClient.readContract.mockResolvedValue("mock_result");
+ provider = new WaapWalletProvider(DEFAULT_CONFIG);
+ });
+
+ // =========================================================
+ // initialization tests
+ // =========================================================
+
+ describe("initialization", () => {
+ it("should create a provider with valid config", () => {
+ expect(provider).toBeInstanceOf(WaapWalletProvider);
+ });
+
+ it("should throw for unsupported chain ID", () => {
+ expect(
+ () =>
+ new WaapWalletProvider({
+ chainId: "999999",
+ }),
+ ).toThrow("Unsupported chain ID: 999999");
+ });
+
+ it("should use default cliPath when not provided", () => {
+ const p = new WaapWalletProvider({ chainId: "8453" });
+ p.getAddress();
+ expect(mockExecFileSync).toHaveBeenCalledWith("waap-cli", ["whoami"], expect.any(Object));
+ });
+ });
+
+ // =========================================================
+ // basic wallet method tests
+ // =========================================================
+
+ describe("basic wallet methods", () => {
+ it("should get the address", () => {
+ const address = provider.getAddress();
+ expect(address).toBe(MOCK_ADDRESS);
+ expect(mockExecFileSync).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["whoami"],
+ expect.any(Object),
+ );
+ });
+
+ it("should cache the address after first call", () => {
+ provider.getAddress();
+ provider.getAddress();
+ const whoamiCalls = mockExecFileSync.mock.calls.filter(
+ call => Array.isArray(call[1]) && call[1].includes("whoami"),
+ );
+ expect(whoamiCalls).toHaveLength(1);
+ });
+
+ it("should get the name", () => {
+ expect(provider.getName()).toBe("waap_wallet_provider");
+ });
+
+ it("should get the network", () => {
+ expect(provider.getNetwork()).toEqual({
+ protocolFamily: "evm",
+ chainId: "8453",
+ networkId: "base-mainnet",
+ });
+ });
+
+ it("should get the balance", async () => {
+ const balance = await provider.getBalance();
+ expect(balance).toBe(BigInt("1000000000000000000"));
+ expect(mockPublicClient.getBalance).toHaveBeenCalledWith({
+ address: MOCK_ADDRESS,
+ });
+ });
+
+ it("should handle connection errors during balance check", async () => {
+ mockPublicClient.getBalance.mockRejectedValueOnce(new Error("Network connection error"));
+ await expect(provider.getBalance()).rejects.toThrow("Network connection error");
+ });
+
+ it("should return the PublicClient", () => {
+ const client = provider.getPublicClient();
+ expect(client).toBe(mockPublicClient);
+ });
+ });
+
+ // =========================================================
+ // signing operation tests
+ // =========================================================
+
+ describe("signing operations", () => {
+ it("should sign a raw hash", async () => {
+ mockCliOutput(`Signature: ${MOCK_SIGNATURE}`);
+ const hash = "0xabcdef1234567890" as `0x${string}`;
+ const result = await provider.sign(hash);
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["sign-message", "--message", hash],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toBe(MOCK_SIGNATURE);
+ });
+
+ it("should sign a text message", async () => {
+ mockCliOutput(`Signature: ${MOCK_SIGNATURE}`);
+ const result = await provider.signMessage("Hello, World!");
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["sign-message", "--message", "Hello, World!"],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toBe(MOCK_SIGNATURE);
+ });
+
+ it("should convert Uint8Array message to hex", async () => {
+ mockCliOutput(`Signature: ${MOCK_SIGNATURE}`);
+ const msg = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
+ await provider.signMessage(msg);
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["sign-message", "--message", "0x48656c6c6f"],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ });
+
+ it("should sign typed data", async () => {
+ mockCliOutput(`Signature: ${MOCK_SIGNATURE}`);
+ const typedData = {
+ domain: { name: "Test", version: "1", chainId: 1 },
+ types: { Person: [{ name: "name", type: "string" }] },
+ primaryType: "Person",
+ message: { name: "Alice" },
+ };
+ const result = await provider.signTypedData(typedData);
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["sign-typed-data", "--data", JSON.stringify(typedData)],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toBe(MOCK_SIGNATURE);
+ });
+
+ it("should handle CLI failure during signing", async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockExecFile.mockImplementation((...args: any[]) => {
+ const cb = args[args.length - 1];
+ if (typeof cb === "function") cb(new Error("waap-cli: command not found"), "", "");
+ });
+ await expect(provider.signMessage("test")).rejects.toThrow("waap-cli: command not found");
+ });
+
+ it("should throw when CLI output contains no hex value", async () => {
+ mockCliOutput("Error: authentication required");
+ await expect(provider.signMessage("test")).rejects.toThrow(
+ "Could not extract hex value from waap-cli output",
+ );
+ });
+ });
+
+ // =========================================================
+ // transaction operation tests
+ // =========================================================
+
+ describe("transaction operations", () => {
+ it("should sign a transaction", async () => {
+ mockCliOutput(`Signed tx: ${MOCK_SIGNED_TX}`);
+ const result = await provider.signTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ value: BigInt("10000000000000000"),
+ });
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ [
+ "sign-tx",
+ "--to",
+ "0xdead000000000000000000000000000000000000",
+ "--value",
+ "0.01",
+ "--chain-id",
+ "8453",
+ "--rpc",
+ "https://mainnet.base.org",
+ ],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toBe(MOCK_SIGNED_TX);
+ });
+
+ it("should send a transaction and return tx hash", async () => {
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ const result = await provider.sendTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ value: BigInt("10000000000000000"),
+ });
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ [
+ "send-tx",
+ "--to",
+ "0xdead000000000000000000000000000000000000",
+ "--value",
+ "0.01",
+ "--chain-id",
+ "8453",
+ "--rpc",
+ "https://mainnet.base.org",
+ ],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toBe(MOCK_TX_HASH);
+ });
+
+ it("should include calldata when provided", async () => {
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ await provider.sendTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ data: "0xabcdef" as `0x${string}`,
+ });
+ expect(mockExecFile).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ [
+ "send-tx",
+ "--to",
+ "0xdead000000000000000000000000000000000000",
+ "--value",
+ "0",
+ "--chain-id",
+ "8453",
+ "--rpc",
+ "https://mainnet.base.org",
+ "--data",
+ "0xabcdef",
+ ],
+ expect.any(Object),
+ expect.any(Function),
+ );
+ });
+
+ it("should omit --rpc when rpcUrl is not configured", async () => {
+ const providerNoRpc = new WaapWalletProvider({
+ chainId: "8453",
+ cliPath: "/usr/local/bin/waap-cli",
+ });
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ await providerNoRpc.sendTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ value: BigInt("10000000000000000"),
+ });
+ const callArgs = mockExecFile.mock.calls.find(
+ call => Array.isArray(call[1]) && call[1].includes("send-tx"),
+ );
+ expect(callArgs![1]).not.toContain("--rpc");
+ });
+
+ it("should default --value to 0 when value is not provided (required by waap-cli for contract calls)", async () => {
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ await provider.sendTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ data: "0x095ea7b3" as `0x${string}`,
+ });
+ const callArgs = mockExecFile.mock.calls.find(
+ call => Array.isArray(call[1]) && call[1].includes("send-tx"),
+ );
+ expect(callArgs![1]).toContain("--value");
+ const valueIdx = callArgs![1].indexOf("--value");
+ expect(callArgs![1][valueIdx + 1]).toBe("0");
+ });
+
+ it("should handle CLI failure during transaction send", async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockExecFile.mockImplementation((...args: any[]) => {
+ const cb = args[args.length - 1];
+ if (typeof cb === "function") cb(new Error("Transaction rejected by policy engine"), "", "");
+ });
+ await expect(
+ provider.sendTransaction({
+ to: "0xdead000000000000000000000000000000000000" as `0x${string}`,
+ value: BigInt("10000000000000000"),
+ }),
+ ).rejects.toThrow("Transaction rejected by policy engine");
+ });
+
+ it("should wait for transaction receipt", async () => {
+ const receipt = await provider.waitForTransactionReceipt(MOCK_TX_HASH as `0x${string}`);
+ expect(receipt.transactionHash).toBe(MOCK_TX_HASH);
+ expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalledWith({
+ hash: MOCK_TX_HASH,
+ timeout: 120_000,
+ });
+ });
+
+ it("should handle receipt timeout errors", async () => {
+ mockPublicClient.waitForTransactionReceipt.mockRejectedValueOnce(new Error("Timed out"));
+ await expect(
+ provider.waitForTransactionReceipt(MOCK_TX_HASH as `0x${string}`),
+ ).rejects.toThrow("Timed out");
+ });
+ });
+
+ // =========================================================
+ // native transfer tests
+ // =========================================================
+
+ describe("nativeTransfer", () => {
+ it("should return the tx hash from send-tx", async () => {
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ const result = await provider.nativeTransfer(
+ "0xdead000000000000000000000000000000000000",
+ "10000000000000000",
+ );
+ expect(result).toBe(MOCK_TX_HASH);
+ });
+
+ it("should not call waitForTransactionReceipt (waap-cli confirms before returning)", async () => {
+ mockCliOutput(`Transaction hash: ${MOCK_TX_HASH}`);
+ await provider.nativeTransfer(
+ "0xdead000000000000000000000000000000000000",
+ "10000000000000000",
+ );
+ expect(mockPublicClient.waitForTransactionReceipt).not.toHaveBeenCalled();
+ });
+ });
+
+ // =========================================================
+ // contract interaction tests
+ // =========================================================
+
+ describe("contract interactions", () => {
+ it("should read contract data", async () => {
+ const abi = [
+ {
+ name: "balanceOf",
+ type: "function",
+ inputs: [{ name: "account", type: "address" }],
+ outputs: [{ name: "balance", type: "uint256" }],
+ stateMutability: "view",
+ },
+ ] as const;
+
+ const result = await provider.readContract({
+ address: "0x1234567890123456789012345678901234567890" as `0x${string}`,
+ abi,
+ functionName: "balanceOf",
+ args: [MOCK_ADDRESS as `0x${string}`],
+ } as unknown as ReadContractParameters);
+
+ expect(result).toBe("mock_result");
+ expect(mockPublicClient.readContract).toHaveBeenCalled();
+ });
+
+ it("should handle contract read errors", async () => {
+ mockPublicClient.readContract.mockRejectedValueOnce(new Error("Contract read error"));
+
+ const abi = [
+ {
+ name: "balanceOf",
+ type: "function",
+ inputs: [{ name: "account", type: "address" }],
+ outputs: [{ name: "balance", type: "uint256" }],
+ stateMutability: "view",
+ },
+ ] as const;
+
+ await expect(
+ provider.readContract({
+ address: "0x1234567890123456789012345678901234567890" as `0x${string}`,
+ abi,
+ functionName: "balanceOf",
+ args: [MOCK_ADDRESS as `0x${string}`],
+ } as unknown as ReadContractParameters),
+ ).rejects.toThrow("Contract read error");
+ });
+ });
+
+ // =========================================================
+ // configureWithWallet tests
+ // =========================================================
+
+ describe("configureWithWallet", () => {
+ it("should log in when credentials are provided", () => {
+ mockCliOutput(`Wallet address: ${MOCK_ADDRESS}`);
+ const result = WaapWalletProvider.configureWithWallet({
+ ...DEFAULT_CONFIG,
+ email: "agent@test.com",
+ password: "secret123",
+ });
+ expect(mockExecFileSync).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["login", "--email", "agent@test.com", "--password", "secret123"],
+ expect.any(Object),
+ );
+ expect(result).toBeInstanceOf(WaapWalletProvider);
+ });
+
+ it("should skip login when no credentials provided", () => {
+ const result = WaapWalletProvider.configureWithWallet(DEFAULT_CONFIG);
+ const loginCalls = mockExecFileSync.mock.calls.filter(
+ call => Array.isArray(call[1]) && call[1].includes("login"),
+ );
+ expect(loginCalls).toHaveLength(0);
+ expect(result).toBeInstanceOf(WaapWalletProvider);
+ });
+
+ it("should support multi-agent isolation via + email notation", () => {
+ mockCliOutput(`Wallet address: ${MOCK_ADDRESS}`);
+ WaapWalletProvider.configureWithWallet({
+ ...DEFAULT_CONFIG,
+ email: "owner+agent007@example.com",
+ password: "secret123",
+ });
+ expect(mockExecFileSync).toHaveBeenCalledWith(
+ DEFAULT_CONFIG.cliPath,
+ ["login", "--email", "owner+agent007@example.com", "--password", "secret123"],
+ expect.any(Object),
+ );
+ });
+
+ it("should handle login failure", () => {
+ mockExecFileSync.mockImplementation(() => {
+ throw new Error("Invalid credentials");
+ });
+ expect(() =>
+ WaapWalletProvider.configureWithWallet({
+ ...DEFAULT_CONFIG,
+ email: "agent@test.com",
+ password: "wrong",
+ }),
+ ).toThrow("Invalid credentials");
+ });
+ });
+
+ // =========================================================
+ // exportWallet tests
+ // =========================================================
+
+ describe("exportWallet", () => {
+ it("should export wallet data with email and chain info", () => {
+ const p = WaapWalletProvider.configureWithWallet({
+ ...DEFAULT_CONFIG,
+ email: "agent@test.com",
+ password: "secret123",
+ });
+ const exported: WaapWalletExport = p.exportWallet();
+ expect(exported.email).toBe("agent@test.com");
+ expect(exported.chainId).toBe("8453");
+ expect(exported.networkId).toBe("base-mainnet");
+ expect(exported.rpcUrl).toBe("https://mainnet.base.org");
+ });
+
+ it("should export undefined email when not configured", () => {
+ const exported = provider.exportWallet();
+ expect(exported.email).toBeUndefined();
+ expect(exported.chainId).toBe("8453");
+ });
+
+ it("should export undefined rpcUrl when not configured", () => {
+ const p = new WaapWalletProvider({ chainId: "8453" });
+ const exported = p.exportWallet();
+ expect(exported.rpcUrl).toBeUndefined();
+ });
+
+ it("exported data is sufficient to reconstruct the provider", () => {
+ const p = WaapWalletProvider.configureWithWallet({
+ ...DEFAULT_CONFIG,
+ email: "agent@test.com",
+ password: "secret123",
+ });
+ const exported = p.exportWallet();
+
+ // Reconstruct — password must be supplied separately (not exported for security)
+ const reconstructed = new WaapWalletProvider({
+ chainId: exported.chainId,
+ rpcUrl: exported.rpcUrl,
+ email: exported.email,
+ });
+ expect(reconstructed.getNetwork().chainId).toBe(exported.chainId);
+ });
+ });
+});
diff --git a/typescript/agentkit/src/wallet-providers/waapWalletProvider.ts b/typescript/agentkit/src/wallet-providers/waapWalletProvider.ts
new file mode 100644
index 000000000..4024211fa
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/waapWalletProvider.ts
@@ -0,0 +1,439 @@
+// TODO: Improve type safety
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { execFile, execFileSync } from "child_process";
+import { promisify } from "util";
+
+const execFileAsync = promisify(execFile);
+import {
+ createPublicClient,
+ http,
+ formatEther,
+ parseEther,
+ TransactionRequest,
+ PublicClient as ViemPublicClient,
+ ReadContractParameters,
+ ReadContractReturnType,
+ Abi,
+ ContractFunctionName,
+ ContractFunctionArgs,
+} from "viem";
+import { EvmWalletProvider } from "./evmWalletProvider";
+import { Network } from "../network";
+import { CHAIN_ID_TO_NETWORK_ID, getChain } from "../network/network";
+
+/**
+ * Exported wallet data from the WaaP wallet provider.
+ * Can be used to reconstruct the provider across sessions.
+ */
+export type WaapWalletExport = {
+ /** The email address associated with the WaaP wallet. */
+ email: string | undefined;
+ /** The EVM chain ID the provider is connected to. */
+ chainId: string;
+ /** The AgentKit network ID corresponding to the chain. */
+ networkId: string | undefined;
+ /** The RPC URL override, if any. */
+ rpcUrl: string | undefined;
+};
+
+/**
+ * Configuration for the WaaP wallet provider.
+ */
+export interface WaapWalletProviderConfig {
+ /** Path to the waap-cli binary. Defaults to "waap-cli". */
+ cliPath?: string;
+
+ /** EVM chain ID (e.g. "8453" for Base). */
+ chainId: string;
+
+ /** RPC URL for the target network. */
+ rpcUrl?: string;
+
+ /** Email for waap-cli login. If provided with password, auto-login on configure. */
+ email?: string;
+
+ /** Password for waap-cli login. */
+ password?: string;
+}
+
+const WAAP_CLI_TIMEOUT = 300_000; // 5 min — send-tx may wait for on-chain confirmation
+
+/**
+ * Executes a waap-cli command synchronously and returns trimmed stdout.
+ * Only used for the login step in configureWithWallet (startup, pre-event-loop).
+ */
+function execWaapCliSync(cliPath: string, args: string[]): string {
+ const result = execFileSync(cliPath, args, {
+ encoding: "utf-8",
+ timeout: WAAP_CLI_TIMEOUT,
+ env: { ...process.env },
+ });
+ return result.trim();
+}
+
+/**
+ * Executes a waap-cli command asynchronously and returns trimmed stdout.
+ * Used for all signing and transaction operations to avoid blocking the event loop.
+ */
+async function execWaapCli(cliPath: string, args: string[]): Promise {
+ const { stdout } = await execFileAsync(cliPath, args, {
+ encoding: "utf-8",
+ timeout: WAAP_CLI_TIMEOUT,
+ env: { ...process.env },
+ });
+ return stdout.trim();
+}
+
+/**
+ * Extracts a hex value (0x...) from waap-cli output, which may contain
+ * emoji decorations and status lines.
+ */
+function extractHex(output: string): `0x${string}` {
+ const match = output.match(/(0x[0-9a-fA-F]+)/);
+ if (!match) {
+ throw new Error(`Could not extract hex value from waap-cli output: ${output}`);
+ }
+ return match[1] as `0x${string}`;
+}
+
+/**
+ * Extracts a value after a label (e.g. "Wallet address: 0x...") from waap-cli output.
+ */
+function extractLabeled(output: string, label: string): string {
+ const regex = new RegExp(`${label}\\s*[:=]\\s*(.+)`, "i");
+ const match = output.match(regex);
+ if (!match) {
+ throw new Error(`Could not find "${label}" in waap-cli output: ${output}`);
+ }
+ return match[1].trim();
+}
+
+/**
+ * Extracts the longest hex value from waap-cli output. Useful when the output
+ * contains multiple hex values (e.g. key IDs, addresses) and we need the
+ * longest one (signatures, signed transactions).
+ */
+function extractLongestHex(output: string): `0x${string}` {
+ const matches = output.match(/0x[0-9a-fA-F]+/g);
+ if (!matches || matches.length === 0) {
+ throw new Error(`Could not extract hex value from waap-cli output: ${output}`);
+ }
+ const longest = matches.reduce((a, b) => (a.length >= b.length ? a : b));
+ return longest as `0x${string}`;
+}
+
+/**
+ * A wallet provider that uses the waap-cli binary for signing operations.
+ *
+ * WaaP (Wallet as a Protocol) manages private keys server-side using
+ * two-party computation, so keys are never exposed locally. This provider
+ * shells out to the waap-cli for all signing operations and uses a Viem
+ * PublicClient for read-only blockchain queries.
+ */
+export class WaapWalletProvider extends EvmWalletProvider {
+ #cliPath: string;
+ #chainId: string;
+ #rpcUrl: string | undefined;
+ #email: string | undefined;
+ #publicClient: ViemPublicClient;
+ #address: string | undefined;
+
+ /**
+ * Constructs a new WaapWalletProvider.
+ *
+ * @param config - The configuration for the wallet provider.
+ */
+ constructor(config: WaapWalletProviderConfig) {
+ super();
+
+ this.#cliPath = config.cliPath || "waap-cli";
+ this.#chainId = config.chainId;
+ this.#rpcUrl = config.rpcUrl;
+ this.#email = config.email;
+
+ const chain = getChain(config.chainId);
+ if (!chain) {
+ throw new Error(`Unsupported chain ID: ${config.chainId}`);
+ }
+
+ this.#publicClient = createPublicClient({
+ chain,
+ transport: config.rpcUrl ? http(config.rpcUrl) : http(),
+ });
+ }
+
+ /**
+ * Creates and configures a WaapWalletProvider. If email and password
+ * are provided, automatically logs in.
+ *
+ * @param config - The configuration for the wallet provider.
+ * @returns A configured WaapWalletProvider instance.
+ */
+ static configureWithWallet(config: WaapWalletProviderConfig): WaapWalletProvider {
+ const cliPath = config.cliPath || "waap-cli";
+
+ if (config.email && config.password) {
+ execWaapCliSync(cliPath, [
+ "login",
+ "--email",
+ config.email,
+ "--password",
+ config.password,
+ ]);
+ }
+
+ const provider = new WaapWalletProvider(config);
+ // Pre-warm address cache synchronously at startup so getAddress() never
+ // blocks the event loop during agent tool execution.
+ provider.getAddress();
+ return provider;
+ }
+
+ /**
+ * Executes a waap-cli command with the configured binary path.
+ */
+ private exec(args: string[]): Promise {
+ return execWaapCli(this.#cliPath, args);
+ }
+
+ /**
+ * Builds common transaction CLI args.
+ */
+ private txArgs(transaction: TransactionRequest): string[] {
+ const args: string[] = [];
+
+ if (transaction.to) {
+ args.push("--to", transaction.to as string);
+ }
+
+ if (transaction.value !== undefined && transaction.value !== null) {
+ // waap-cli expects ETH, AgentKit passes Wei
+ const ethValue = formatEther(BigInt(transaction.value.toString()));
+ args.push("--value", ethValue);
+ } else {
+ // waap-cli requires --value even for contract calls (e.g. ERC20 approve)
+ args.push("--value", "0");
+ }
+
+ args.push("--chain-id", this.#chainId);
+
+ if (this.#rpcUrl) {
+ args.push("--rpc", this.#rpcUrl);
+ }
+
+ if (transaction.data) {
+ args.push("--data", transaction.data as string);
+ }
+
+ return args;
+ }
+
+ /**
+ * Gets the address of the wallet.
+ *
+ * @returns The wallet address.
+ */
+ getAddress(): string {
+ if (!this.#address) {
+ // Fallback: fetch synchronously (only on first call if not pre-warmed).
+ // Prefer calling configureWithWallet() which pre-warms the address cache.
+ const output = execWaapCliSync(this.#cliPath, ["whoami"]);
+ this.#address = extractLabeled(output, "Wallet address");
+ }
+ return this.#address;
+ }
+
+ /**
+ * Gets the network of the wallet.
+ *
+ * @returns The network of the wallet.
+ */
+ getNetwork(): Network {
+ return {
+ protocolFamily: "evm" as const,
+ chainId: this.#chainId,
+ networkId: CHAIN_ID_TO_NETWORK_ID[Number(this.#chainId)],
+ };
+ }
+
+ /**
+ * Gets the name of the wallet provider.
+ *
+ * @returns The name of the wallet provider.
+ */
+ getName(): string {
+ return "waap_wallet_provider";
+ }
+
+ /**
+ * Exports the wallet data for session persistence.
+ *
+ * The exported data contains enough information to reconstruct the provider
+ * in a future session by passing it back to `configureWithWallet`. Note that
+ * the password is not included — store it separately and securely.
+ *
+ * @returns The wallet export data.
+ */
+ exportWallet(): WaapWalletExport {
+ return {
+ email: this.#email,
+ chainId: this.#chainId,
+ networkId: this.getNetwork().networkId,
+ rpcUrl: this.#rpcUrl,
+ };
+ }
+
+ /**
+ * Gets the balance of the wallet.
+ *
+ * @returns The balance in Wei.
+ */
+ async getBalance(): Promise {
+ return this.#publicClient.getBalance({
+ address: this.getAddress() as `0x${string}`,
+ });
+ }
+
+ /**
+ * Signs a raw hash using waap-cli sign-message.
+ *
+ * @param hash - The hash to sign.
+ * @returns The signature.
+ */
+ async sign(hash: `0x${string}`): Promise<`0x${string}`> {
+ const output = await this.exec(["sign-message", "--message", hash]);
+ return extractLongestHex(output);
+ }
+
+ /**
+ * Signs a message using EIP-191 (personal_sign).
+ *
+ * @param message - The message to sign.
+ * @returns The signature.
+ */
+ async signMessage(message: string | Uint8Array): Promise<`0x${string}`> {
+ let msgArg: string;
+ if (message instanceof Uint8Array) {
+ msgArg = "0x" + Buffer.from(message).toString("hex");
+ } else {
+ msgArg = message;
+ }
+ const output = await this.exec(["sign-message", "--message", msgArg]);
+ return extractLongestHex(output);
+ }
+
+ /**
+ * Signs EIP-712 typed data.
+ *
+ * @param typedData - The typed data to sign.
+ * @returns The signature.
+ */
+ async signTypedData(typedData: any): Promise<`0x${string}`> {
+ const output = await this.exec(["sign-typed-data", "--data", JSON.stringify(typedData)]);
+ return extractLongestHex(output);
+ }
+
+ /**
+ * Signs a transaction without broadcasting it.
+ *
+ * @param transaction - The transaction to sign.
+ * @returns The signed transaction hex.
+ */
+ async signTransaction(transaction: TransactionRequest): Promise<`0x${string}`> {
+ const output = await this.exec(["sign-tx", ...this.txArgs(transaction)]);
+ // Try labeled extraction first ("Signed transaction:" or "Signed tx:"), then
+ // fall back to the longest hex value — the signed RLP blob is always longer
+ // than any address or hash that waap-cli may print alongside it.
+ for (const label of ["Signed transaction", "Signed tx"]) {
+ try {
+ return extractLabeled(output, label) as `0x${string}`;
+ } catch {
+ // try next label
+ }
+ }
+ // Fallback: longest hex value in output
+ const matches = output.match(/0x[0-9a-fA-F]+/g);
+ if (matches) {
+ const longest = matches.reduce((a, b) => (a.length >= b.length ? a : b));
+ return longest as `0x${string}`;
+ }
+ return extractHex(output);
+ }
+
+ /**
+ * Sends a transaction (sign + broadcast).
+ *
+ * @param transaction - The transaction to send.
+ * @returns The transaction hash.
+ */
+ async sendTransaction(transaction: TransactionRequest): Promise<`0x${string}`> {
+ const output = await this.exec(["send-tx", ...this.txArgs(transaction)]);
+ // Use labeled extraction to avoid picking up the sender address that
+ // waap-cli prints before the transaction hash.
+ try {
+ return extractLabeled(output, "Transaction hash") as `0x${string}`;
+ } catch {
+ // Fall back to last hex value in output if label not found
+ const matches = output.match(/0x[0-9a-fA-F]{64}/g);
+ if (matches) return matches[matches.length - 1] as `0x${string}`;
+ return extractHex(output);
+ }
+ }
+
+ /**
+ * Waits for a transaction receipt.
+ *
+ * @param txHash - The transaction hash.
+ * @returns The transaction receipt.
+ */
+ async waitForTransactionReceipt(txHash: `0x${string}`): Promise {
+ return this.#publicClient.waitForTransactionReceipt({
+ hash: txHash,
+ timeout: 120_000, // 2 min — avoids hanging indefinitely on slow networks
+ });
+ }
+
+ /**
+ * Reads a contract.
+ *
+ * @param params - The parameters to read the contract.
+ * @returns The response from the contract.
+ */
+ async readContract<
+ const abi extends Abi | readonly unknown[],
+ functionName extends ContractFunctionName,
+ const args extends ContractFunctionArgs,
+ >(
+ params: ReadContractParameters,
+ ): Promise> {
+ return this.#publicClient.readContract(params);
+ }
+
+ /**
+ * Gets the Viem PublicClient for read-only operations.
+ *
+ * @returns The PublicClient instance.
+ */
+ getPublicClient(): ViemPublicClient {
+ return this.#publicClient;
+ }
+
+ /**
+ * Transfers native currency (ETH, etc.).
+ *
+ * @param to - The destination address.
+ * @param value - The amount in Wei.
+ * @returns The transaction hash.
+ */
+ async nativeTransfer(to: string, value: string): Promise {
+ // waap-cli send-tx waits for on-chain confirmation before returning the hash,
+ // so we skip waitForTransactionReceipt to avoid a redundant second block wait.
+ const txHash = await this.sendTransaction({
+ to: to as `0x${string}`,
+ value: BigInt(value),
+ });
+
+ return txHash;
+ }
+}
diff --git a/typescript/examples/langchain-waap-chatbot/.env-local b/typescript/examples/langchain-waap-chatbot/.env-local
new file mode 100644
index 000000000..c1a50bcdc
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/.env-local
@@ -0,0 +1,14 @@
+OPENAI_API_KEY=
+
+# WaaP Configuration
+WAAP_EMAIL=
+WAAP_PASSWORD=
+
+# Optional: Path to waap-cli binary (default: "waap-cli")
+WAAP_CLI_PATH=
+
+# Optional: EVM chain ID (default: "84532" for Base Sepolia)
+WAAP_CHAIN_ID=
+
+# Optional: Custom RPC URL
+WAAP_RPC_URL=
diff --git a/typescript/examples/langchain-waap-chatbot/.eslintrc.json b/typescript/examples/langchain-waap-chatbot/.eslintrc.json
new file mode 100644
index 000000000..91571ba7a
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/.eslintrc.json
@@ -0,0 +1,4 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "extends": ["../../.eslintrc.base.json"]
+}
diff --git a/typescript/examples/langchain-waap-chatbot/.prettierrc b/typescript/examples/langchain-waap-chatbot/.prettierrc
new file mode 100644
index 000000000..ffb416b74
--- /dev/null
+++ b/typescript/examples/langchain-waap-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-waap-chatbot/README.md b/typescript/examples/langchain-waap-chatbot/README.md
new file mode 100644
index 000000000..0f9451030
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/README.md
@@ -0,0 +1,75 @@
+# WaaP AgentKit LangChain Extension Examples - Chatbot Typescript
+
+This example demonstrates an agent setup as a terminal-style chatbot with a [WaaP (Wallet as a Protocol)](https://waap.xyz) wallet.
+
+WaaP uses two-party computation (2PC) to split private keys between the client and server, so keys are never fully exposed in any single location. The AgentKit integration wraps the `waap-cli` binary behind the standard wallet provider interface.
+
+## Ask the chatbot to engage in the Web3 ecosystem!
+
+- "Transfer a portion of your ETH to a random address"
+- "What is the price of BTC?"
+- "What kind of wallet do you have?"
+- "What is your wallet address?"
+
+## Requirements
+
+- [Node.js 18+](https://nodejs.org/en/download/current)
+- [waap-cli](https://www.npmjs.com/package/@human.tech/waap-cli) installed globally or available on `$PATH`
+- A WaaP account (sign up via `waap-cli signup --email you@example.com`)
+
+### Install waap-cli
+
+```bash
+npm install -g @human.tech/waap-cli
+```
+
+### Checking Node Version
+
+```bash
+node --version
+npm --version
+```
+
+## Installation
+
+```bash
+npm install
+```
+
+## Run the Chatbot
+
+### Set ENV Vars
+
+Create a `.env` file (or copy `.env-local`) with the following variables:
+
+```bash
+# Required
+OPENAI_API_KEY= # OpenAI API key for the LLM
+WAAP_EMAIL= # WaaP account email
+WAAP_PASSWORD= # WaaP account password
+
+# Optional
+WAAP_CLI_PATH= # Path to waap-cli binary (default: "waap-cli")
+WAAP_CHAIN_ID= # EVM chain ID (default: "84532" for Base Sepolia)
+WAAP_RPC_URL= # Custom RPC URL for the chain
+```
+
+#### Multi-Agent Isolation
+
+WaaP supports creating isolated wallets for each agent using `+` email notation:
+
+```bash
+WAAP_EMAIL=owner+agent007@example.com
+```
+
+Each `+` alias gets its own wallet, allowing multiple agents to operate independently under a single account.
+
+### Start the chatbot
+
+```bash
+npm start
+```
+
+## License
+
+[Apache-2.0](../../../LICENSE.md)
diff --git a/typescript/examples/langchain-waap-chatbot/chatbot.ts b/typescript/examples/langchain-waap-chatbot/chatbot.ts
new file mode 100644
index 000000000..435437a02
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/chatbot.ts
@@ -0,0 +1,292 @@
+import {
+ AgentKit,
+ WaapWalletProvider,
+ wethActionProvider,
+ walletActionProvider,
+ erc20ActionProvider,
+ pythActionProvider,
+} 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";
+import fs from "fs";
+
+dotenv.config();
+
+const WALLET_DATA_FILE = "wallet_data.txt";
+
+/**
+ * Validates that required environment variables are set
+ *
+ * @throws {Error} - If required environment variables are missing
+ * @returns {void}
+ */
+function validateEnvironment(): void {
+ const missingVars: string[] = [];
+
+ const requiredVars = ["OPENAI_API_KEY", "WAAP_EMAIL", "WAAP_PASSWORD"];
+ requiredVars.forEach(varName => {
+ if (!process.env[varName]) {
+ missingVars.push(varName);
+ }
+ });
+
+ if (missingVars.length > 0) {
+ console.error("Error: Required environment variables are not set");
+ missingVars.forEach(varName => {
+ console.error(`${varName}=your_${varName.toLowerCase()}_here`);
+ });
+ process.exit(1);
+ }
+
+ if (!process.env.WAAP_CHAIN_ID) {
+ console.warn("Warning: WAAP_CHAIN_ID not set, defaulting to 84532 (Base Sepolia)");
+ }
+}
+
+// Add this right after imports and before any other code
+validateEnvironment();
+
+/**
+ * Initialize the agent with WaaP AgentKit
+ *
+ * @returns Agent executor and config
+ */
+async function initializeAgent() {
+ try {
+ // Initialize LLM
+ const llm = new ChatOpenAI({
+ model: "gpt-4o-mini",
+ });
+
+ const chainId = process.env.WAAP_CHAIN_ID || "84532";
+
+ // Configure WaaP wallet provider with auto-login
+ const walletProvider = WaapWalletProvider.configureWithWallet({
+ cliPath: process.env.WAAP_CLI_PATH,
+ chainId,
+ rpcUrl: process.env.WAAP_RPC_URL,
+ email: process.env.WAAP_EMAIL,
+ password: process.env.WAAP_PASSWORD,
+ });
+
+ console.log(`WaaP wallet address: ${walletProvider.getAddress()}`);
+ console.log(`Network: ${JSON.stringify(walletProvider.getNetwork())}`);
+
+ // Initialize AgentKit
+ const agentkit = await AgentKit.from({
+ walletProvider,
+ actionProviders: [
+ wethActionProvider(),
+ pythActionProvider(),
+ walletActionProvider(),
+ erc20ActionProvider(),
+ ],
+ });
+
+ const tools = await getLangChainTools(agentkit);
+
+ // Store buffered conversation history in memory
+ const memory = new MemorySaver();
+ const agentConfig = { configurable: { thread_id: "WaaP AgentKit Chatbot Example!" } };
+
+ // Create React Agent using the LLM and WaaP AgentKit tools
+ const agent = createAgent({
+ model: llm,
+ tools,
+ checkpointer: memory,
+ systemPrompt: `
+ You are a helpful agent with a WaaP (Wallet as a Protocol) wallet that can interact onchain
+ using the Coinbase Developer Platform AgentKit. Your wallet uses two-party computation (2PC)
+ for key security - private keys are never fully exposed in any single location.
+
+ You are empowered to interact onchain using your tools. If you ever need funds, you can
+ request them from the faucet if you are on network ID 'base-sepolia'. If not, you can
+ provide your wallet details and request funds from the user. Before executing your first
+ action, get the wallet details to see what network you're on. If there is a 5XX (internal)
+ HTTP error code, ask the user to try again later. If someone asks you to do something you
+ can't do with your currently available tools, you must say so, and encourage them to
+ implement it themselves using the CDP SDK + AgentKit, recommend they go to
+ docs.cdp.coinbase.com for more information. Be concise and helpful with your responses.
+ Refrain from restating your tools' descriptions unless it is explicitly requested.
+ `,
+ });
+
+ // Save wallet data
+ const exportedWallet = walletProvider.exportWallet();
+ fs.writeFileSync(WALLET_DATA_FILE, JSON.stringify(exportedWallet));
+
+ return { agent, config: agentConfig };
+ } catch (error) {
+ console.error("Failed to initialize agent:", error);
+ throw error;
+ }
+}
+
+/**
+ * Run the agent autonomously with specified intervals
+ *
+ * @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 = 10) {
+ console.log("Starting autonomous mode...");
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ try {
+ const thought =
+ "Be creative and do something interesting on the blockchain. " +
+ "Choose an action or set of actions and execute it that highlights your abilities.";
+
+ 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);
+ }
+ }
+}
+
+/**
+ * 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.");
+
+ 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();
+ }
+}
+
+/**
+ * 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 WaaP Agent...");
+ main().catch(error => {
+ console.error("Fatal error:", error);
+ process.exit(1);
+ });
+}
diff --git a/typescript/examples/langchain-waap-chatbot/package.json b/typescript/examples/langchain-waap-chatbot/package.json
new file mode 100644
index 000000000..c125a12dc
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@coinbase/langchain-waap-chatbot-example",
+ "description": "WaaP AgentKit LangChain Extension Chatbot Example",
+ "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 -c .prettierrc --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-waap-chatbot/tsconfig.json b/typescript/examples/langchain-waap-chatbot/tsconfig.json
new file mode 100644
index 000000000..a37da3664
--- /dev/null
+++ b/typescript/examples/langchain-waap-chatbot/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "preserveSymlinks": true,
+ "outDir": "./dist",
+ "rootDir": ".",
+ "module": "Node16"
+ },
+ "include": ["*.ts"]
+}
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/.env-local b/typescript/examples/vercel-ai-sdk-waap-chatbot/.env-local
new file mode 100644
index 000000000..c1a50bcdc
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/.env-local
@@ -0,0 +1,14 @@
+OPENAI_API_KEY=
+
+# WaaP Configuration
+WAAP_EMAIL=
+WAAP_PASSWORD=
+
+# Optional: Path to waap-cli binary (default: "waap-cli")
+WAAP_CLI_PATH=
+
+# Optional: EVM chain ID (default: "84532" for Base Sepolia)
+WAAP_CHAIN_ID=
+
+# Optional: Custom RPC URL
+WAAP_RPC_URL=
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/.eslintrc.json b/typescript/examples/vercel-ai-sdk-waap-chatbot/.eslintrc.json
new file mode 100644
index 000000000..91571ba7a
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/.eslintrc.json
@@ -0,0 +1,4 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "extends": ["../../.eslintrc.base.json"]
+}
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/.prettierignore b/typescript/examples/vercel-ai-sdk-waap-chatbot/.prettierignore
new file mode 100644
index 000000000..20de531f4
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/.prettierignore
@@ -0,0 +1,7 @@
+docs/
+dist/
+coverage/
+.github/
+src/client
+**/**/*.json
+*.md
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/.prettierrc b/typescript/examples/vercel-ai-sdk-waap-chatbot/.prettierrc
new file mode 100644
index 000000000..ffb416b74
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-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/vercel-ai-sdk-waap-chatbot/README.md b/typescript/examples/vercel-ai-sdk-waap-chatbot/README.md
new file mode 100644
index 000000000..23804ee7f
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/README.md
@@ -0,0 +1,86 @@
+# WaaP AgentKit Vercel AI SDK Extension Examples - Chatbot Typescript
+
+This example demonstrates an agent setup as a terminal-style chatbot with a [WaaP (Wallet as a Protocol)](https://waap.xyz) wallet.
+
+WaaP uses two-party computation (2PC) to split private keys between the client and server, so keys are never fully exposed in any single location. The AgentKit integration wraps the `waap-cli` binary behind the standard wallet provider interface.
+
+## Ask the chatbot to engage in the Web3 ecosystem!
+
+- "Transfer a portion of your ETH to a random address"
+- "What is the price of BTC?"
+- "What kind of wallet do you have?"
+- "What is your wallet address?"
+- "Sign this message: AgentKit WAAP smoke test"
+- "Sign EIP-712 typed data for this payload: ..."
+- "Read `balanceOf` from this ERC-20 contract for my wallet address"
+
+## Requirements
+
+- [Node.js 18+](https://nodejs.org/en/download/current)
+- [waap-cli](https://www.npmjs.com/package/@human.tech/waap-cli) installed globally or available on `$PATH`
+- A WaaP account (sign up via `waap-cli signup --email you@example.com`)
+
+### Install waap-cli
+
+```bash
+npm install -g @human.tech/waap-cli
+```
+
+### Checking Node Version
+
+```bash
+node --version
+npm --version
+```
+
+### API Keys
+
+You'll need:
+
+- [OpenAI API Key](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)
+- WaaP account credentials (`WAAP_EMAIL` and `WAAP_PASSWORD`)
+
+Once you have them, rename `.env-local` to `.env` and set:
+
+```bash
+# Required
+OPENAI_API_KEY=
+WAAP_EMAIL=
+WAAP_PASSWORD=
+
+# Optional
+WAAP_CLI_PATH=
+WAAP_CHAIN_ID=
+WAAP_RPC_URL=
+```
+
+## Running the example
+
+From the root directory, run:
+
+```bash
+npm install
+npm run build
+```
+
+This installs dependencies and builds packages locally. The chatbot uses local `@coinbase/agentkit-vercel-ai-sdk` and `@coinbase/agentkit`.
+
+Now from the `typescript/examples/vercel-ai-sdk-waap-chatbot` directory, run:
+
+```bash
+npm start
+```
+
+Select "1. chat mode" and start telling your agent to do things onchain.
+
+## Prompts to test advanced WAAP actions
+
+After startup, try these prompts:
+
+- `Sign this message: AgentKit WAAP smoke test at 2026-04-13T10:00:00Z`
+- `Sign EIP-712 typed data with domain {"name":"AgentKitTest","version":"1","chainId":11155111,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"}, types {"Test":[{"name":"contents","type":"string"},{"name":"value","type":"uint256"}]}, primaryType "Test", and message {"contents":"hello","value":"123","from":""}`
+- `Read contract 0x... using ABI [{"name":"balanceOf","type":"function","stateMutability":"view","inputs":[{"name":"account","type":"address"}],"outputs":[{"name":"","type":"uint256"}]}], function balanceOf, args ["0x..."]`
+
+## License
+
+[Apache-2.0](../../../LICENSE.md)
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/chatbot.ts b/typescript/examples/vercel-ai-sdk-waap-chatbot/chatbot.ts
new file mode 100644
index 000000000..2e01eb9c4
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/chatbot.ts
@@ -0,0 +1,375 @@
+import {
+ AgentKit,
+ WaapWalletProvider,
+ EvmWalletProvider,
+ customActionProvider,
+ erc20ActionProvider,
+ pythActionProvider,
+ walletActionProvider,
+ wethActionProvider,
+} from "@coinbase/agentkit";
+import { getVercelAITools } from "@coinbase/agentkit-vercel-ai-sdk";
+import { openai } from "@ai-sdk/openai";
+import { streamText, ToolSet, stepCountIs } from "ai";
+import * as dotenv from "dotenv";
+import * as fs from "fs";
+import * as readline from "readline";
+import { z } from "zod";
+
+dotenv.config();
+
+const WALLET_DATA_FILE = "wallet_data.txt";
+
+const SignMessageSchema = z.object({
+ message: z.string().describe("The message to sign."),
+});
+
+const SignTypedDataSchema = z.object({
+ typedData: z.record(z.string(), z.any()).optional().describe("Full EIP-712 typed data object."),
+ typedDataJson: z.string().optional().describe("Stringified JSON for EIP-712 typed data."),
+ domain: z.record(z.string(), z.any()).optional().describe("EIP-712 domain object."),
+ types: z
+ .record(z.string(), z.array(z.object({ name: z.string(), type: z.string() })))
+ .optional()
+ .describe("EIP-712 type definitions."),
+ primaryType: z.string().optional().describe("Primary type name for typed data."),
+ message: z.record(z.string(), z.any()).optional().describe("EIP-712 message payload."),
+});
+
+const ReadContractSchema = z.object({
+ contractAddress: z.string().describe("The contract address to query."),
+ abi: z.array(z.record(z.string(), z.any())).describe("Contract ABI array."),
+ functionName: z.string().describe("Name of the view/pure function to call."),
+ args: z.array(z.any()).optional().describe("Function arguments."),
+});
+
+const waapAdvancedActions = customActionProvider([
+ {
+ name: "sign_message",
+ description:
+ "Sign an arbitrary message with the connected WaaP wallet using personal_sign semantics.",
+ schema: SignMessageSchema,
+ invoke: async (walletProvider, args) => {
+ const signature = await walletProvider.signMessage(args.message);
+ return `Message signature: ${signature}`;
+ },
+ },
+ {
+ name: "sign_typed_data",
+ description:
+ "Sign EIP-712 typed data with the connected WaaP wallet. Input must include domain, types, primaryType, and message.",
+ schema: SignTypedDataSchema,
+ invoke: async (walletProvider, args) => {
+ let typedDataPayload: Record | undefined = args.typedData;
+
+ if (!typedDataPayload && args.typedDataJson) {
+ typedDataPayload = JSON.parse(args.typedDataJson) as Record;
+ }
+
+ if (!typedDataPayload && args.domain && args.types && args.primaryType && args.message) {
+ typedDataPayload = {
+ domain: args.domain,
+ types: args.types,
+ primaryType: args.primaryType,
+ message: args.message,
+ };
+ }
+
+ if (!typedDataPayload) {
+ throw new Error(
+ "Missing typed data. Provide typedData, typedDataJson, or domain/types/primaryType/message.",
+ );
+ }
+
+ const signature = await walletProvider.signTypedData(typedDataPayload);
+ return `Typed data signature: ${signature}`;
+ },
+ },
+ {
+ name: "read_contract",
+ description:
+ "Read a pure/view function from a smart contract using contract address, ABI, function name, and args.",
+ schema: ReadContractSchema,
+ invoke: async (walletProvider, args) => {
+ const result = await walletProvider.readContract({
+ address: args.contractAddress as `0x${string}`,
+ abi: args.abi,
+ functionName: args.functionName,
+ args: args.args ?? [],
+ });
+ return `Contract read result: ${JSON.stringify(result, (_key, value) =>
+ typeof value === "bigint" ? value.toString() : value,
+ )}`;
+ },
+ },
+]);
+
+/**
+ * Validates required environment variables.
+ */
+function validateEnvironment(): void {
+ const missingVars: string[] = [];
+
+ const requiredVars = ["OPENAI_API_KEY", "WAAP_EMAIL", "WAAP_PASSWORD"];
+ requiredVars.forEach(varName => {
+ if (!process.env[varName]) {
+ missingVars.push(varName);
+ }
+ });
+
+ if (missingVars.length > 0) {
+ console.error("Error: Required environment variables are not set");
+ missingVars.forEach(varName => {
+ console.error(`${varName}=your_${varName.toLowerCase()}_here`);
+ });
+ process.exit(1);
+ }
+
+ if (!process.env.WAAP_CHAIN_ID) {
+ console.warn("Warning: WAAP_CHAIN_ID not set, defaulting to 84532 (Base Sepolia)");
+ }
+}
+
+validateEnvironment();
+
+const system = `You are a helpful agent with a WaaP (Wallet as a Protocol) wallet that can interact onchain
+using the Coinbase Developer Platform AgentKit. Your wallet uses two-party computation (2PC)
+for key security - private keys are never fully exposed in any single location.
+
+You are empowered to interact onchain using your tools. If you ever need funds, you can request
+them from the faucet if you are on network ID 'base-sepolia'. If not, you can provide your wallet
+details and request funds from the user. Before executing your first action, get the wallet details
+to see what network you're on. If there is a 5XX (internal) HTTP error code, ask the user to try
+again later. If someone asks you to do something you can't do with your currently available tools,
+you must say so, and encourage them to implement it themselves using the CDP SDK + AgentKit,
+recommend they go to docs.cdp.coinbase.com for more information. Be concise and helpful with your
+responses. Refrain from restating your tools' descriptions unless it is explicitly requested.`;
+
+const signingInstructions = `When the user asks to sign a message or sign EIP-712 typed data, you must call the appropriate signing tool directly.
+Do not refuse unless a tool call actually fails. If a signing tool fails, return the exact error and ask for corrected input.`;
+
+/**
+ * Initializes AgentKit with WaaP wallet and actions.
+ *
+ * @returns Initialized Vercel AI SDK tools.
+ */
+async function initializeAgent() {
+ try {
+ const chainId = process.env.WAAP_CHAIN_ID || "84532";
+
+ const walletProvider = WaapWalletProvider.configureWithWallet({
+ cliPath: process.env.WAAP_CLI_PATH,
+ chainId,
+ rpcUrl: process.env.WAAP_RPC_URL,
+ email: process.env.WAAP_EMAIL,
+ password: process.env.WAAP_PASSWORD,
+ });
+
+ console.log(`WaaP wallet address: ${walletProvider.getAddress()}`);
+ console.log(`Network: ${JSON.stringify(walletProvider.getNetwork())}`);
+
+ const agentKit = await AgentKit.from({
+ walletProvider,
+ actionProviders: [
+ wethActionProvider(),
+ pythActionProvider(),
+ walletActionProvider(),
+ erc20ActionProvider(),
+ waapAdvancedActions,
+ ],
+ });
+
+ const tools = getVercelAITools(agentKit);
+
+ const exportedWallet = walletProvider.exportWallet();
+ fs.writeFileSync(WALLET_DATA_FILE, JSON.stringify(exportedWallet));
+
+ return { tools };
+ } catch (error) {
+ console.error("Failed to initialize agent:", error);
+ throw error;
+ }
+}
+
+/**
+ * Runs interactive chat mode.
+ *
+ * @param tools - Vercel AI SDK tools.
+ */
+async function runChatMode(tools: ToolSet) {
+ console.log("Starting chat mode... Type 'exit' to end.");
+
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+
+ const question = (prompt: string): Promise =>
+ new Promise(resolve => rl.question(prompt, resolve));
+
+ const messages: Parameters[0]["messages"] = [];
+
+ try {
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const userInput = await question("\nPrompt: ");
+ console.log("-------------------");
+
+ if (userInput.toLowerCase() === "exit") {
+ break;
+ }
+
+ messages.push({ role: "user", content: userInput });
+
+ const result = streamText({
+ model: openai.chat("gpt-4o-mini"),
+ messages,
+ tools,
+ system: `${system}\n\n${signingInstructions}`,
+ stopWhen: stepCountIs(10),
+ onStepFinish: async ({ toolResults }) => {
+ for (const tr of toolResults) {
+ console.log(`Tool ${tr.toolName}: ${tr.output}`);
+ }
+ },
+ });
+
+ let fullResponse = "";
+ for await (const delta of result.textStream) {
+ fullResponse += delta;
+ }
+
+ if (fullResponse) {
+ console.log("\n Response: " + fullResponse);
+ }
+
+ messages.push({ role: "assistant", content: fullResponse });
+
+ console.log("-------------------");
+ }
+ } catch (error) {
+ console.error("Error:", error);
+ } finally {
+ rl.close();
+ }
+}
+
+/**
+ * Runs autonomous mode on an interval.
+ *
+ * @param tools - Vercel AI SDK tools.
+ * @param interval - Seconds between autonomous steps.
+ */
+async function runAutonomousMode(tools: ToolSet, interval = 10) {
+ console.log("Starting autonomous mode...");
+
+ const messages: Parameters[0]["messages"] = [];
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ try {
+ const thought =
+ "Be creative and do something interesting on the blockchain. " +
+ "Choose an action or set of actions and execute it that highlights your abilities.";
+
+ messages.push({ role: "user", content: thought });
+
+ const result = streamText({
+ model: openai.chat("gpt-4o-mini"),
+ messages,
+ tools,
+ system: `${system}\n\n${signingInstructions}`,
+ stopWhen: stepCountIs(10),
+ onStepFinish: async ({ toolResults }) => {
+ for (const tr of toolResults) {
+ console.log(`Tool ${tr.toolName}: ${tr.output}`);
+ }
+ },
+ });
+
+ let fullResponse = "";
+ for await (const delta of result.textStream) {
+ fullResponse += delta;
+ }
+
+ if (fullResponse) {
+ console.log("\n Response: " + fullResponse);
+ }
+
+ messages.push({ role: "assistant", content: fullResponse });
+
+ console.log("-------------------");
+
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error("Error:", error.message);
+ }
+ process.exit(1);
+ }
+ }
+}
+
+/**
+ * Prompts user to choose chat or autonomous mode.
+ *
+ * @returns Selected execution 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.");
+ }
+}
+
+/**
+ * Main entry point.
+ */
+async function main() {
+ try {
+ const { tools } = await initializeAgent();
+ const mode = await chooseMode();
+
+ if (mode === "chat") {
+ await runChatMode(tools);
+ } else {
+ await runAutonomousMode(tools);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error("Error:", error.message);
+ }
+ process.exit(1);
+ }
+}
+
+if (require.main === module) {
+ console.log("Starting WaaP Agent...");
+ main().catch(error => {
+ console.error("Fatal error:", error);
+ process.exit(1);
+ });
+}
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/package.json b/typescript/examples/vercel-ai-sdk-waap-chatbot/package.json
new file mode 100644
index 000000000..dfb2ba0b6
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@coinbase/vercel-ai-sdk-waap-chatbot-example",
+ "description": "WaaP AgentKit Vercel AI SDK Chatbot Example",
+ "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 -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"",
+ "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\""
+ },
+ "dependencies": {
+ "@ai-sdk/openai": "^3.0.0",
+ "@coinbase/agentkit": "workspace:*",
+ "@coinbase/agentkit-vercel-ai-sdk": "workspace:*",
+ "ai": "^6.0.0",
+ "dotenv": "^16.4.5",
+ "tsx": "^4.7.1",
+ "zod": "^4.0.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.0",
+ "tsx": "^4.7.1"
+ }
+}
diff --git a/typescript/examples/vercel-ai-sdk-waap-chatbot/tsconfig.json b/typescript/examples/vercel-ai-sdk-waap-chatbot/tsconfig.json
new file mode 100644
index 000000000..a37da3664
--- /dev/null
+++ b/typescript/examples/vercel-ai-sdk-waap-chatbot/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "preserveSymlinks": true,
+ "outDir": "./dist",
+ "rootDir": ".",
+ "module": "Node16"
+ },
+ "include": ["*.ts"]
+}