LangChain example
A complete agent: a real model, a real tool call, traced end to end. Two files, no build step.
Run it
Save both files below into an empty folder, then:
npm install export MATRIX_API_KEY=sk_... # from matrixverify.dev, shown once export ANTHROPIC_API_KEY=sk-ant-... export AGENT_EMAIL=you@yourdomain.com node agent.mjs
On Windows PowerShell, set each variable with $env:MATRIX_API_KEY="sk_..." instead of export.
What happens
The agent is asked to email an invoice. It calls send_email, the tool reports success, and the agent tells you it sent it. The tool in this example sends nothing — it returns success without doing anything, which is the failure Matrix exists to catch. Swap in your real send when you are done looking.
The trace — instruction, tool call with its arguments, and the agent’s claim — appears on your findings page within about ten minutes. Whether the claim can be checked against a mailbox depends on one thing: during early access, connecting your mailbox is done by hand. Until it is, an email claim backed by a send call comes back inconclusive, not guessed. Get in touch to connect yours.
package.json
{
"name": "matrix-langchain-example",
"private": true,
"type": "module",
"description": "A LangChain agent traced by Matrix — npm install && node agent.mjs",
"scripts": {
"start": "node agent.mjs"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"@langchain/anthropic": "^1.5.10",
"@langchain/core": "^1.2.11",
"@matrixverify/verify": "^0.1.0",
"langchain": "^1.5.11",
"zod": "^4.5.4"
}
}
agent.mjs
/**
* A LangChain agent traced by Matrix.
*
* npm install
* MATRIX_API_KEY=sk_... ANTHROPIC_API_KEY=sk-ant-... AGENT_EMAIL=you@yourdomain.com node agent.mjs
*
* The agent is asked to email an invoice. It calls send_email, the tool reports
* success, and the agent says it sent it. The trace — what it was asked, what
* it called with which arguments, and what it claimed — lands on your findings
* page within about ten minutes.
*
* THE send_email TOOL BELOW SENDS NOTHING. It returns success without doing
* anything, which is the failure Matrix exists to catch. Replace it with your
* real send once you have seen a trace arrive.
*/
import { ChatAnthropic } from "@langchain/anthropic";
import { createAgent, tool } from "langchain";
import { z } from "zod";
import { verify } from "@matrixverify/verify";
import { verifyCallbackHandler } from "@matrixverify/verify/langchain";
// The SDK is fail-silent by design — a missing key would look like success —
// so this example checks up front and says so.
for (const name of ["MATRIX_API_KEY", "ANTHROPIC_API_KEY"]) {
if (!process.env[name]) {
console.error(`Set ${name} first. See the comment at the top of this file.`);
process.exit(1);
}
}
verify.init({
apiKey: process.env.MATRIX_API_KEY,
// Explicit, so this works on @matrixverify/verify 0.1.0 as well, where the
// default pointed at localhost.
endpoint: process.env.MATRIX_ENDPOINT ?? "https://matrixverify.dev/api/traces",
});
const sendEmail = tool(
async ({ to, subject }) => {
// Replace with your real send. This one does nothing and reports success.
return JSON.stringify({ status: "sent", to, subject });
},
{
// Matrix matches tool names like send_email, gmail.send_email, email.send.
// A tool named something else is reported as a setup problem.
name: "send_email",
description: "Send an email to one recipient.",
schema: z.object({
to: z.string().describe("Recipient email address"),
subject: z.string(),
body: z.string(),
}),
}
);
// --- model ---
const model = new ChatAnthropic({ model: "claude-opus-5", maxTokens: 16000 });
// --- end model ---
const agent = createAgent({
model,
tools: [sendEmail],
// A plain one-sentence report is what Matrix reads as the claim. Phrasing
// like "Emailed the August invoice to dana@…" is recognised; a vaguer
// "all done!" gives it nothing to verify.
systemPrompt:
"You are an operations assistant. Use your tools to do what is asked. " +
"When you are done, reply with one plain sentence stating exactly what you " +
"did, including the recipient's email address.",
});
const handler = verifyCallbackHandler({
// Callbacks cannot discover which account acted. Without this, every email
// claim comes back account_unverified.
fromResolver: () => process.env.AGENT_EMAIL ?? null,
});
const to = process.argv[2] ?? "dana@northwind.example";
const instruction = `Email the August invoice to ${to}.`;
console.log(`asked: ${instruction}`);
try {
const result = await agent.invoke(
{ messages: [{ role: "user", content: instruction }] },
{ callbacks: [handler] }
);
const last = result.messages.at(-1);
const said =
typeof last?.content === "string"
? last.content
: (last?.content ?? [])
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("\n");
console.log(`agent said: ${said}`);
} finally {
// Flushes the buffer. Nothing is sent without it.
await verify.shutdown();
}
console.log("\nTrace sent. It appears at https://matrixverify.dev within about ten minutes.");