Skip to main content

LangChain.js Integration

Build LangChain.js agents that make verifiable HTTP requests to external services using Beltic signing.

Overview

LangChain.js uses a tool-based architecture where agents invoke tools to perform actions. By creating a signed HTTP tool, your agent can make verifiable requests to any external API.

Prerequisites

  • Node.js 18+
  • LangChain.js installed
  • Beltic CLI installed and authenticated (beltic login)
  • Agent keypair generated (beltic keygen)
  • Key directory hosted at an HTTPS URL

Installation

npm install langchain @langchain/core @langchain/anthropic @belticlabs/agent-signer zod

Creating a Signed API Tool

Create a reusable tool that wraps HTTP requests with Beltic signing:
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { signHttpRequest, importPrivateKey } from "@belticlabs/agent-signer";

// Load credentials once
const privateKeyPem = process.env.AGENT_PRIVATE_KEY!;
const keyId = process.env.AGENT_KEY_ID!;
const keyDirectoryUrl = process.env.AGENT_KEY_DIRECTORY_URL!;

export const signedApiCall = tool(
  async ({ method, url, body, headers }) => {
    const privateKey = await importPrivateKey(privateKeyPem, "EdDSA");

    const signedHeaders = await signHttpRequest({
      method,
      url,
      headers: { "content-type": "application/json", ...headers },
      body: body ? JSON.stringify(body) : undefined,
      privateKey,
      keyId,
      keyDirectoryUrl,
    });

    const response = await fetch(url, {
      method,
      headers: { ...headers, ...signedHeaders },
      body: body ? JSON.stringify(body) : undefined,
    });

    const data = await response.json();

    return JSON.stringify({
      status: response.status,
      data,
    });
  },
  {
    name: "signed_api_call",
    description:
      "Make a signed HTTP request to an external API. The request will be cryptographically signed so the receiving service can verify it came from a trusted agent.",
    schema: z.object({
      method: z
        .enum(["GET", "POST", "PUT", "DELETE"])
        .describe("HTTP method"),
      url: z.string().url().describe("The API endpoint URL"),
      body: z
        .record(z.unknown())
        .optional()
        .describe("Request body for POST/PUT requests"),
      headers: z
        .record(z.string())
        .optional()
        .describe("Additional HTTP headers"),
    }),
  }
);

Using with LangChain Agents

Basic Agent Setup

import { ChatAnthropic } from "@langchain/anthropic";
import { createToolCallingAgent, AgentExecutor } from "langchain/agents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { signedApiCall } from "./tools/signed-api-call";

// Initialize the model
const model = new ChatAnthropic({
  model: "claude-sonnet-4-5-20250929",
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
});

// Create the prompt
const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant that can make API calls on behalf of the user. When making API calls, always use the signed_api_call tool to ensure requests are verifiable."],
  ["human", "{input}"],
  ["placeholder", "{agent_scratchpad}"],
]);

// Create the agent with the signed API tool
const agent = createToolCallingAgent({
  llm: model,
  tools: [signedApiCall],
  prompt,
});

const executor = new AgentExecutor({
  agent,
  tools: [signedApiCall],
});

// Run the agent
const result = await executor.invoke({
  input: "Check my account balance at https://api.example.com/balance",
});

console.log(result.output);

Multiple Tools Example

Combine signed API calls with other tools:
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { signedApiCall } from "./tools/signed-api-call";

// A simple calculation tool
const calculateTool = tool(
  async ({ expression }) => {
    return String(eval(expression));
  },
  {
    name: "calculate",
    description: "Evaluate a mathematical expression",
    schema: z.object({
      expression: z.string().describe("The math expression to evaluate"),
    }),
  }
);

// Create agent with multiple tools
const agent = createToolCallingAgent({
  llm: model,
  tools: [signedApiCall, calculateTool],
  prompt,
});

const executor = new AgentExecutor({
  agent,
  tools: [signedApiCall, calculateTool],
});

// Agent can now make signed API calls AND do calculations
const result = await executor.invoke({
  input: "Get my balance from https://api.example.com/balance and calculate 10% of it",
});

Specialized API Tools

For cleaner code, create domain-specific tools:
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { signHttpRequest, importPrivateKey } from "@belticlabs/agent-signer";

// Payments API tool
export const paymentsApi = tool(
  async ({ action, amount, recipient }) => {
    const privateKey = await importPrivateKey(process.env.AGENT_PRIVATE_KEY!, "EdDSA");

    const url = "https://api.payments.com/v1/transactions";
    const body = JSON.stringify({ action, amount, recipient });

    const signedHeaders = await signHttpRequest({
      method: "POST",
      url,
      headers: { "content-type": "application/json" },
      body,
      privateKey,
      keyId: process.env.AGENT_KEY_ID!,
      keyDirectoryUrl: process.env.AGENT_KEY_DIRECTORY_URL!,
    });

    const response = await fetch(url, {
      method: "POST",
      headers: signedHeaders,
      body,
    });

    return response.json();
  },
  {
    name: "payments_api",
    description: "Interact with the payments API to check balance or send money",
    schema: z.object({
      action: z.enum(["balance", "send"]).describe("The action to perform"),
      amount: z.number().optional().describe("Amount for send action"),
      recipient: z.string().optional().describe("Recipient for send action"),
    }),
  }
);

Server-Side Verification

The receiving service verifies requests using @belticlabs/verifier:
import express from "express";
import { createAgentAuthMiddleware, scopeGuard } from "@belticlabs/verifier/express";

const app = express();
app.use(express.json());

// Add Beltic verification middleware
app.use("/v1", createAgentAuthMiddleware());

// Protect routes with scope requirements
app.post("/v1/transactions", scopeGuard("payments:write"), (req, res) => {
  const { action, amount, recipient } = req.body;

  // req.agent contains verified agent info
  console.log("Verified agent:", req.agent.id);
  console.log("Developer:", req.agent.developer.legalName);

  if (action === "balance") {
    return res.json({ balance: 1000 });
  }

  if (action === "send") {
    return res.json({ success: true, transactionId: "tx_123" });
  }

  res.status(400).json({ error: "Invalid action" });
});

app.listen(3000);

Environment Variables

Set these environment variables:
# Anthropic API key for LangChain
export ANTHROPIC_API_KEY="sk-ant-..."

# Your agent's private key (PEM format)
export AGENT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----"

# JWK thumbprint of your public key
export AGENT_KEY_ID="S9Zz0..."

# URL where your key directory is hosted
export AGENT_KEY_DIRECTORY_URL="https://your-agent.com/.well-known/http-message-signatures-directory"

Error Handling

Add proper error handling to your tools:
export const signedApiCall = tool(
  async ({ method, url, body, headers }) => {
    try {
      const privateKey = await importPrivateKey(privateKeyPem, "EdDSA");

      const signedHeaders = await signHttpRequest({
        method,
        url,
        headers: { "content-type": "application/json", ...headers },
        body: body ? JSON.stringify(body) : undefined,
        privateKey,
        keyId,
        keyDirectoryUrl,
      });

      const response = await fetch(url, {
        method,
        headers: { ...headers, ...signedHeaders },
        body: body ? JSON.stringify(body) : undefined,
      });

      if (!response.ok) {
        return JSON.stringify({
          error: true,
          status: response.status,
          message: await response.text(),
        });
      }

      return JSON.stringify({
        status: response.status,
        data: await response.json(),
      });
    } catch (error) {
      return JSON.stringify({
        error: true,
        message: error instanceof Error ? error.message : "Unknown error",
      });
    }
  },
  {
    name: "signed_api_call",
    description: "Make a signed HTTP request to an external API",
    schema: z.object({
      method: z.enum(["GET", "POST", "PUT", "DELETE"]),
      url: z.string().url(),
      body: z.record(z.unknown()).optional(),
      headers: z.record(z.string()).optional(),
    }),
  }
);

Next Steps