LangChain has become the standard for building reasoning loops, but out of the box, it lacks a secure wallet primitive. Most tutorials suggest passing a private key directly to a Tool. This is dangerous for production.
This guide shows you how to wrap a LangChain Tool with PolicyLayer to enforce hard spending limits.
The Risk of Raw Keys in Tools
When you define a Custom Tool in LangChain for blockchain interactions, it usually looks like this:
class SendEthTool extends Tool {
name = "send_eth";
description = "Sends ETH to an address";
async _call(input: string) {
// DANGER: No limits here!
const wallet = new Wallet(process.env.PRIVATE_KEY);
return wallet.sendTransaction(...);
}
}
The problems with this approach:
- No spending limits — The LLM can send any amount
- No recipient validation — Any address is valid
- No velocity controls — Infinite transactions possible
- No audit trail — No record of what was attempted vs executed
If the LLM hallucinates the input amount or the recipient, the transaction executes immediately. A single prompt injection could drain the entire wallet.
The Solution: Policy-Wrapped Tools
Instead of giving tools direct wallet access, we wrap them with PolicyLayer. The LLM interacts with the tool normally, but every transaction passes through policy enforcement.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
│ LangChain │────▶│ Secure │────▶│ PolicyLayer │────▶│ Blockchain│
│ Agent │ │ Tool │ │ (2-Gate) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └──────────┘
Step 1: Install Dependencies
npm install @policylayer/sdk langchain @langchain/openai ethers
Step 2: Create the Policy-Aware Tool
We modify the tool to use PolicyLayer's Two-Gate Enforcement:
import { PolicyWallet, createEthersAdapter } from '@policylayer/sdk';
import { Tool } from 'langchain/tools';
class SecureSendTool extends Tool {
name = "secure_send_eth";
description = "Safely sends ETH with spending limits. Input: JSON with 'to' (address) and 'amount' (in wei)";
private wallet: PolicyWallet;
constructor(wallet: PolicyWallet) {
super();
this.wallet = wallet;
}
async _call(input: string): Promise<string> {
let parsed;
try {
parsed = JSON.parse(input);
} catch {
return "ERROR: Invalid JSON input. Expected {to: address, amount: string}";
}
const { to, amount } = parsed;
if (!to || !amount) {
return "ERROR: Missing required fields 'to' and 'amount'";
}
try {
// PolicyWallet.send() handles both gates automatically:
// 1. Validates intent against spending limits
// 2. Verifies fingerprint to prevent tampering
// 3. Signs and broadcasts only if approved
const result = await this.wallet.send({
chain: 'ethereum',
asset: 'eth',
to,
amount
});
return `SUCCESS: Transaction sent. Hash: ${result.hash}`;
} catch (error: any) {
// Policy denials have code POLICY_DECISION_DENY
// The specific reason is in error.message
if (error.code === 'POLICY_DECISION_DENY') {
if (error.message.includes('DAILY_LIMIT')) {
return `BLOCKED: Daily spending limit reached. Try again tomorrow.`;
}
if (error.message.includes('PER_TX_LIMIT')) {
return `BLOCKED: Amount exceeds per-transaction limit.`;
}
if (error.message.includes('RECIPIENT_NOT_WHITELISTED')) {
return `BLOCKED: Recipient address not approved.`;
}
}
return `BLOCKED: ${error.message}`;
}
}
}
Step 3: Set Up the Agent
Create the complete LangChain agent with policy-protected tools:
import { ChatOpenAI } from '@langchain/openai';
import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents';
import { PolicyWallet, createEthersAdapter } from '@policylayer/sdk';
async function createSecureAgent() {
// 1. Create policy-wrapped wallet
const adapter = await createEthersAdapter(
process.env.PRIVATE_KEY!,
process.env.RPC_URL!
);
const policyWallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!
});
// 2. Create secure tools
const tools = [
new SecureSendTool(policyWallet),
// Add other policy-wrapped tools...
];
// 3. Create agent
const llm = new ChatOpenAI({ modelName: 'gpt-4' });
const agent = await createOpenAIToolsAgent({
llm,
tools,
prompt: yourPromptTemplate
});
// 4. Create executor
return new AgentExecutor({
agent,
tools,
verbose: true
});
}
// Usage
const agent = await createSecureAgent();
const result = await agent.invoke({
input: "Send 0.1 ETH to 0xBob..."
});
Step 4: Configure Policies
Set up your spending limits in the PolicyLayer dashboard or via API:
// Example policy configuration
const policy = {
perTransactionLimit: '100000000000000000', // 0.1 ETH max per tx
dailyLimit: '1000000000000000000', // 1 ETH max per day
hourlyLimit: '500000000000000000', // 0.5 ETH max per hour
allowedRecipients: [ // Optional whitelist
'0xAlice...',
'0xBob...',
'0xContract...'
]
};
Multi-Tool Scenarios
Most agents need multiple financial tools. Each should be policy-wrapped:
const tools = [
new SecureSendEthTool(policyWallet),
new SecureSendUsdcTool(policyWallet),
new SecureSwapTool(policyWallet),
new CheckBalanceTool(policyWallet), // Read-only, no policy needed
];
PolicyLayer tracks spending across all tools. If the agent uses SendEth and SendUsdc in the same day, both count towards the daily limit.
Error Handling Best Practices
Return structured error messages so the LLM can reason about failures:
async _call(input: string): Promise<string> {
try {
const result = await this.wallet.send(/* ... */);
return JSON.stringify({
status: 'success',
hash: result.hash,
amount: result.amount
});
} catch (error: any) {
return JSON.stringify({
status: 'blocked',
reason: error.code,
message: error.message,
suggestion: getSuggestion(error.code)
});
}
}
function getSuggestion(code: string): string {
switch (code) {
case 'DAILY_LIMIT_EXCEEDED':
return 'Wait until tomorrow or request limit increase';
case 'PER_TX_LIMIT_EXCEEDED':
return 'Split into smaller transactions';
case 'RECIPIENT_NOT_WHITELISTED':
return 'Request recipient approval from admin';
default:
return 'Contact support';
}
}
Works with Other Frameworks
The same pattern applies to other agent frameworks:
- CrewAI — Wrap tools the same way
- AutoGPT — Integrate via plugin system
- Custom agents — Any code calling wallet functions
The key insight: policy enforcement happens at the wallet layer, not the agent layer. This means you can swap agent frameworks without changing your security model.
Why This Matters
By wrapping the execution logic:
- The Agent is oblivious — The LLM just tries to use the tool
- The Policy is sovereign — If the LLM tries to send 100 ETH, PolicyLayer blocks it before signing
- Failures are graceful — The agent receives an error message it can reason about
- Audit trail is complete — Every attempt is logged, successful or not
This is the only way to safely deploy LangChain agents that handle real value on mainnet.
Related reading:
Ready to secure your AI agents?
- Quick Start Guide - Get running in 5 minutes
- GitHub - Open source SDK
Top comments (0)