Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/nodejs-langgraph-agent/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# ─── Anthropic (the LLM driving the agent) ──────────────────────────────────
ANTHROPIC_API_KEY=sk-ant-your-key-here

# ─── Dynamic environment (server-side MPC signing) ──────────────────────────
DYNAMIC_ENVIRONMENT_ID=your_environment_id
DYNAMIC_API_KEY=your_dynamic_api_key
8 changes: 8 additions & 0 deletions examples/nodejs-langgraph-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
dist/
.env
.env.*
!.example.env
*.log
.DS_Store
.wallet-state.json
80 changes: 80 additions & 0 deletions examples/nodejs-langgraph-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Bare-bones LangGraph agent + Dynamic server wallet

A minimal [LangGraph](https://github.com/langchain-ai/langgraphjs) ReAct agent
that acts through its own **Dynamic server-side MPC wallet**. The agent creates
and owns its wallet entirely server-side — no user JWT, no delegation, no
client-side approval flow required.

It ships three tools:

| Tool | What it does |
| --- | --- |
| `list_wallets` | Returns the agent's server wallet address. |
| `get_token_balances` | Native-token balances across EVM chains via on-chain RPC. |
| `send_transaction` | Native transfer, signed via Dynamic server-side MPC. **Always** gated behind a `y/N` confirm prompt. |

## How it works

```
You ──▶ LangGraph ReAct agent (Claude) ──▶ tools ──▶ Dynamic MPC signing ──▶ chain
```

- **`src/wallet.ts`** — initializes the `DynamicEvmWalletClient`, creates a new
wallet on first run (persisted to `.wallet-state.json`), and signs + broadcasts
transactions.
- **`src/tools.ts`** — the three LangChain tools above.
- **`src/agent.ts`** — the `createReactAgent` loop (Claude Haiku 4.5) + system prompt.
- **`src/index.ts`** — an interactive terminal REPL.
- **`src/confirm.ts`** — the confirmation prompt for sensitive actions.

## Setup

1. Install dependencies (this repo uses pnpm):

```bash
pnpm install
```

2. Create your env file from the example and fill it in:

```bash
cp .example.env .env
```

| Variable | Purpose |
| --- | --- |
| `ANTHROPIC_API_KEY` | Drives the agent. |
| `DYNAMIC_ENVIRONMENT_ID`, `DYNAMIC_API_KEY` | Dynamic env for server-side MPC signing. |

3. Run it:

```bash
pnpm start # or: pnpm dev (watch mode)
```

On first run the agent creates an MPC wallet and saves it to `.wallet-state.json`.
Subsequent runs reuse the same wallet.

```
You: show my wallet
Agent: Your server wallet is 0x1234…abcd.

You: send 0.001 ETH on ethereum to 0xabc…
┌─ ACTION REQUIRED ────────────────────────────────────────
│ Send native transfer
│ Chain: Ethereum (1)
│ To: 0xabc…
│ Amount: 0.001 ETH
└──────────────────────────────────────────────────────────
Proceed? [y/N]
```

## Notes

- **Mainnet only.** Supported chains: Ethereum (1), Polygon (137), Base (8453),
Arbitrum (42161), Optimism (10), BSC (56). Extend `CHAIN_MAP` in `src/wallet.ts`.
- `.wallet-state.json` contains the server key share — treat it like a private key.
It is gitignored; never commit it or expose it to untrusted parties.
- `send_transaction` only does native transfers. To add ERC-20 transfers or contract
calls, build the calldata and extend `sendTransactionServer`.
- Conversation memory is in-process (`MemorySaver`) and resets on restart.
28 changes: 28 additions & 0 deletions examples/nodejs-langgraph-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "nodejs-langgraph-agent",
"version": "1.0.0",
"private": true,
"description": "Bare-bones LangGraph ReAct agent backed by a Dynamic server-side MPC wallet",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dynamic-labs-wallet/node": "1.0.25",
"@dynamic-labs-wallet/node-evm": "1.0.25",
"@langchain/anthropic": "1.4.0",
"@langchain/core": "1.1.47",
"@langchain/langgraph": "1.3.2",
"dotenv": "17.4.2",
"langchain": "1.4.1",
"viem": "2.50.4",
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "25.9.1",
"tsx": "4.22.3",
"typescript": "6.0.3"
}
}
2,930 changes: 2,930 additions & 0 deletions examples/nodejs-langgraph-agent/pnpm-lock.yaml

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions examples/nodejs-langgraph-agent/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ChatAnthropic } from "@langchain/anthropic";
import { createAgent } from "langchain";
import { HumanMessage } from "@langchain/core/messages";
import { MemorySaver } from "@langchain/langgraph";
import { allTools } from "./tools.js";

const SYSTEM_PROMPT =
"You are a Web3 assistant with access to an agent-owned EVM wallet via Dynamic server-side MPC.\n\n" +
"## The agent wallet\n" +
"There is one wallet: the agent's own server wallet, created and held server-side by this agent. " +
"Use list_wallets to get the address. When the user says 'my wallet' or 'the wallet', this is it. " +
"The same address works on every EVM chain.\n\n" +
"## Capabilities\n" +
"- get_token_balances: check native token balances across EVM chains (on-chain RPC). " +
"Pass networkId to filter to a specific chain.\n" +
"- send_transaction: send a native transfer. The user is always prompted to confirm " +
"before anything is broadcast.\n\n" +
"## Rules\n" +
"- Mainnet only — no testnets.\n" +
"- Always resolve the wallet address with list_wallets before acting on 'my wallet'.\n" +
"- Never invent balances or transaction hashes — only report tool results.";

// Built lazily on first use so startup credential checks run (and report) first.
let _agent: ReturnType<typeof createAgent> | null = null;

function getAgent() {
if (!_agent) {
_agent = createAgent({
model: new ChatAnthropic({ model: "claude-haiku-4-5-20251001", temperature: 0 }),
tools: allTools,
checkpointer: new MemorySaver(),
systemPrompt: SYSTEM_PROMPT,
});
}
return _agent;
}

export async function runAgent(
userMessage: string,
threadId: string = "default"
): Promise<string> {
const result = await getAgent().invoke(
{ messages: [new HumanMessage(userMessage)] },
{ configurable: { thread_id: threadId } }
);
const last = result.messages[result.messages.length - 1];
return typeof last.content === "string" ? last.content : JSON.stringify(last.content);
}
37 changes: 37 additions & 0 deletions examples/nodejs-langgraph-agent/src/confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type readline from "node:readline";

// ─── Shared readline instance ─────────────────────────────────────────────────

let _rl: readline.Interface | null = null;

export function setReadlineForConfirm(rl: readline.Interface): void {
_rl = rl;
}

// ─── Confirmation prompt ──────────────────────────────────────────────────────

const WIDTH = 58;

/** Prompts the user to confirm a sensitive action. Defaults to deny. */
export async function confirm(summary: string): Promise<boolean> {
const bar = "─".repeat(WIDTH);

process.stdout.write(`\n┌─ ACTION REQUIRED ${bar.slice(18)}\n`);
for (const line of summary.split("\n")) {
process.stdout.write(`│ ${line}\n`);
}
process.stdout.write(`└${bar}\n`);

return new Promise((resolve) => {
if (!_rl) {
process.stdout.write("No readline available — action denied.\n");
resolve(false);
return;
}
_rl.question("Proceed? [y/N] ", (answer) => {
const confirmed = answer.trim().toLowerCase() === "y";
if (!confirmed) process.stdout.write("Cancelled.\n");
resolve(confirmed);
});
});
}
51 changes: 51 additions & 0 deletions examples/nodejs-langgraph-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import "dotenv/config";
import readline from "node:readline";
import { initServerWallet } from "./wallet.js";
import { setAgentWallet } from "./tools.js";
import { runAgent } from "./agent.js";
import { setReadlineForConfirm } from "./confirm.js";

// ─── Initialize the agent's server wallet ────────────────────────────────────

const wallet = await initServerWallet();
setAgentWallet(wallet);

// ─── Interactive REPL ─────────────────────────────────────────────────────────

console.log("=".repeat(60));
console.log(" Dynamic + LangGraph bare-bones agent");
console.log("=".repeat(60));
console.log("Example commands:");
console.log(' "show my wallet"');
console.log(' "what are my token balances"');
console.log(' "send 0.001 ETH on ethereum to 0x..."');
console.log(" Type 'exit' to quit\n");

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
setReadlineForConfirm(rl);

const threadId = "interactive-session";

function prompt(question: string): Promise<string> {
return new Promise((resolve) => rl.question(question, resolve));
}

rl.on("close", () => {
console.log("\nGoodbye!");
process.exit(0);
});

while (true) {
const input = (await prompt("You: ")).trim();
if (!input) continue;
if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
rl.close();
break;
}
try {
const response = await runAgent(input, threadId);
console.log(`\nAgent: ${response}\n`);
} catch (err: any) {
console.error(`\nError: ${err?.message ?? String(err)}\n`);
}
}
Loading