Skip to content

Node.js SDK

The Layr8 Node.js SDK is a TypeScript/ESM library for building DIDComm agents. It handles WebSocket connections, Phoenix Channel protocol, DIDComm message formatting, and thread correlation. You write domain logic.

A basic agent is ~30 lines of TypeScript. The SDK manages everything between your handler and the network.

Install

Terminal window
npm install @layr8/sdk

Requires Node.js >= 20 and ESM ("type": "module" in package.json).

Core Concepts

Config

import { Layr8Client, logErrors } from "@layr8/sdk";
const client = new Layr8Client(logErrors(), {
nodeUrl: "wss://your-node.layr8.cloud/plugin_socket/websocket",
apiKey: "your-api-key",
agentDid: "did:web:your-node.layr8.cloud:my-agent",
});

Layr8Client takes a required ErrorHandler as its first argument, followed by an optional config object. The error handler is called for SDK-level errors that cannot be returned to a direct caller (unparseable messages, missing handlers, server rejections). logErrors is a built-in helper that logs all errors via console.error.

All config fields fall back to environment variables if empty: LAYR8_NODE_URL, LAYR8_API_KEY, LAYR8_AGENT_DID. If agentDid is omitted, the node assigns an ephemeral DID on connect.

Handlers

Register handlers before calling connect. The handler’s return value determines what happens:

import type { Message } from "@layr8/sdk";
client.handle(
"https://layr8.io/protocols/echo/1.0/request",
async (msg: Message): Promise<Message | null> => {
// Return Message → send response to sender
// Return null → no response (fire-and-forget)
// Throw → send problem report to sender
},
);

The protocol base URI is derived automatically from the message type and registered with the node on connect.

Send

await client.send({
type: "https://didcomm.org/basicmessage/2.0/message",
to: ["did:web:partner-node:agent"],
body: { content: "Hello!" },
});

By default, send waits for the server to acknowledge the message and throws if the server rejects it (e.g., authorization failure). For true fire-and-forget behavior:

await client.send(
{
type: "https://didcomm.org/basicmessage/2.0/message",
to: ["did:web:partner-node:agent"],
body: { content: "Hello!" },
},
{ fireAndForget: true },
);

send accepts Partial<Message> — only type, to, and body are required.

Request (Request/Response)

const resp = await client.request(
{
type: "https://layr8.io/protocols/echo/1.0/request",
to: ["did:web:partner-node:echo-agent"],
body: { message: "ping" },
},
{ signal: AbortSignal.timeout(5_000) },
);
// resp is the correlated response, matched by thread ID

Messages

interface Message {
id: string; // auto-generated if empty
type: string; // DIDComm message type URI
from: string; // auto-filled with agent DID
to: string[]; // recipient DIDs
threadId: string; // auto-generated for request
parentThreadId: string;
body: unknown; // serialized to JSON
context?: MessageContext;
}

Use unmarshalBody<T>(msg) to deserialize the body with type safety. On inbound messages, msg.context provides node metadata: authorized, recipient, and senderCredentials.

Verifiable Credentials

The SDK provides methods for signing, verifying, storing, and retrieving W3C Verifiable Credentials through the cloud-node’s REST API.

Sign a Credential

import type { Credential } from "@layr8/sdk";
const cred: Credential = {
"@context": ["https://www.w3.org/ns/credentials/v2"],
type: ["VerifiableCredential"],
credentialSubject: { id: subjectDid, name: "Acme Corp" },
};
const signedJwt = await client.signCredential(cred);

The issuer DID defaults to client.did. Override with { issuerDid: "did:web:..." }. Output format defaults to compact_jwt; pass { format: "json" } for JSON-LD.

Verify a Credential

const result = await client.verifyCredential(signedJwt);
// result.credential — decoded credential claims
// result.headers — JWT headers

Store, List, and Get

const stored = await client.storeCredential(signedJwt);
// stored.id — unique credential ID
const creds = await client.listCredentials();
// StoredCredential[]
const fetched = await client.getCredential(stored.id);
// fetched.credential_jwt — the original signed JWT

Sign and Verify a Presentation

const presJwt = await client.signPresentation([signedJwt]);
const result = await client.verifyPresentation(presJwt);
// result.presentation — decoded presentation
// result.headers — JWT headers

See Verifiable Credentials for background on the W3C standard and trust model.

Durable Handlers (Persist-then-Ack)

For messages that must not be lost, use manual ack to persist before acknowledging:

import { ack } from "@layr8/sdk";
client.handle(
"https://layr8.io/protocols/order/1.0/created",
async (msg: Message): Promise<null> => {
// Write to disk first — if this throws, the message is
// NOT acked and the cloud-node will redeliver it.
appendFileSync("messages.jsonl", JSON.stringify(msg) + "\n");
// The cast is needed because ack() uses an internal type
// with the ack callback attached at runtime.
ack(msg as any);
return null;
},
{ manualAck: true },
);

See examples/durable-handler.ts for a complete working example.

Error Handling

The SDK requires an ErrorHandler at construction time. This ensures SDK-level errors are never silently swallowed.

ErrorHandler

ErrorHandler is called for errors that cannot be returned to a direct caller:

import type { ErrorHandler } from "@layr8/sdk";
import { ErrorKind, SDKError } from "@layr8/sdk";

Error kinds:

KindWhen
ErrorKind.ParseFailureInbound message could not be parsed as a DIDComm envelope
ErrorKind.NoHandlerNo handler registered for the message type
ErrorKind.HandlerExceptionA handler threw an exception
ErrorKind.ServerRejectThe server rejected a message (e.g., authorization failure)
ErrorKind.TransportWriteFailed to write to the WebSocket connection

Built-in Helper

logErrors logs all SDK errors via console.error:

import { Layr8Client, logErrors } from "@layr8/sdk";
const client = new Layr8Client(logErrors());

Custom Handler

For production agents, route errors to your observability stack:

const client = new Layr8Client((error) => {
logger.error("sdk error", {
kind: ErrorKind[error.kind],
messageId: error.messageId,
type: error.type,
from: error.from,
cause: error.cause,
});
});

Connection Resilience

The SDK automatically reconnects when the WebSocket connection drops (e.g., node restart, network interruption). Reconnection uses exponential backoff starting at 1 second, capped at 30 seconds.

During reconnection:

  • send(), request(), and other operations throw NotConnectedError immediately — the SDK does not queue messages
  • The disconnect event fires when the connection drops
  • The reconnect event fires when the connection is restored
  • close() stops the reconnect loop

Claude Code Skill

The SDK includes a Claude Code skill that teaches Claude the full API. Install it into any agent project:

Terminal window
mkdir -p .claude/skills
cp /path/to/node-sdk/.claude/skills/build-layr8-agent-node.md .claude/skills/

Once installed, Claude Code knows how to build Layr8 agents in every session. Example prompts:

  • “Build me a webhook relay agent that forwards DIDComm messages to my REST API”
  • “Add a handler for order.created messages that validates the sender’s credentials”
  • “Convert this Express endpoint into a DIDComm agent”