Beltic logo
SDK GuideIntegrations

LangChain.js

Integrate Beltic signing with LangChain.js agents and tools

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