DEV Community

L_X_1
L_X_1

Posted on • Originally published at policylayer.com

How to Set Spending Limits for LangChain Agents on Ethereum

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(...);
  }
}
Enter fullscreen mode Exit fullscreen mode

The problems with this approach:

  1. No spending limits — The LLM can send any amount
  2. No recipient validation — Any address is valid
  3. No velocity controls — Infinite transactions possible
  4. 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)   │     │          │
└─────────────┘     └─────────────┘     └─────────────┘     └──────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Dependencies

npm install @policylayer/sdk langchain @langchain/openai ethers
Enter fullscreen mode Exit fullscreen mode

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}`;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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..."
});
Enter fullscreen mode Exit fullscreen mode

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...'
  ]
};
Enter fullscreen mode Exit fullscreen mode

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
];
Enter fullscreen mode Exit fullscreen mode

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The Agent is oblivious — The LLM just tries to use the tool
  2. The Policy is sovereign — If the LLM tries to send 100 ETH, PolicyLayer blocks it before signing
  3. Failures are graceful — The agent receives an error message it can reason about
  4. 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?

Top comments (0)