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
npm install @layr8/sdkRequires 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 IDMessages
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 headersStore, 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 JWTSign and Verify a Presentation
const presJwt = await client.signPresentation([signedJwt]);
const result = await client.verifyPresentation(presJwt);// result.presentation — decoded presentation// result.headers — JWT headersSee 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:
| Kind | When |
|---|---|
ErrorKind.ParseFailure | Inbound message could not be parsed as a DIDComm envelope |
ErrorKind.NoHandler | No handler registered for the message type |
ErrorKind.HandlerException | A handler threw an exception |
ErrorKind.ServerReject | The server rejected a message (e.g., authorization failure) |
ErrorKind.TransportWrite | Failed 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 throwNotConnectedErrorimmediately — the SDK does not queue messages- The
disconnectevent fires when the connection drops - The
reconnectevent 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:
mkdir -p .claude/skillscp /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”
Links
- Node.js SDK on GitHub — Full README, source code, and examples
- Example Agents — Working demo agents from beginner to advanced
- DIDComm Reference — Protocol details and message patterns
- Getting Started — End-to-end setup walkthrough