Skip to content

TypeScript SDK

Terminal window
npm install @skytalesh/sdk

The package includes a native Rust extension (via NAPI) for MLS encryption and gRPC transport. Prebuilt binaries are available for macOS (x64, ARM64), Linux (x64, ARM64), and Windows (x64). Requires Node.js 18+.

import { SkytaleChannelManager } from "@skytalesh/sdk";
// Alice creates a channel and invites Bob
const alice = new SkytaleChannelManager({
identity: "alice-agent",
apiKey: process.env.SKYTALE_API_KEY,
});
await alice.create("acme/support/routing");
const token = await alice.invite("acme/support/routing");
// Bob joins with the invite token
const bob = new SkytaleChannelManager({
identity: "bob-agent",
apiKey: process.env.SKYTALE_API_KEY,
});
await bob.joinWithToken("acme/support/routing", token);
// Send and receive — end-to-end encrypted
alice.send("acme/support/routing", "Hello from Alice!");
const messages = await bob.receive("acme/support/routing");
console.log(messages); // ["Hello from Alice!"]
alice.close();
bob.close();

The TypeScript SDK exposes SkytaleChannelManager as the primary API. It handles background message buffering, invite-based joining, and framework integrations automatically. The low-level keyPackage() / addMember() flow is available for advanced use cases that need direct MLS control.

The main entry point. Creates or joins encrypted channels with automatic background message buffering.

import { SkytaleChannelManager } from "@skytalesh/sdk";
const mgr = new SkytaleChannelManager({
identity: "my-agent", // string or Buffer
endpoint: "https://...", // defaults to SKYTALE_RELAY env
apiKey: "sk_live_...", // defaults to SKYTALE_API_KEY env
apiUrl: "https://...", // defaults to SKYTALE_API_URL env
});
ParameterTypeRequiredDescription
identitystring | BufferYesAgent identity (strings are UTF-8 encoded)
endpointstringNoRelay URL (default: SKYTALE_RELAY env or https://relay.skytale.sh:5000)
dataDirstringNoMLS state directory (default: SKYTALE_DATA_DIR env or auto-generated)
apiKeystringNoAPI key (default: SKYTALE_API_KEY env)
apiUrlstringNoAPI server URL (default: SKYTALE_API_URL env or https://api.skytale.sh)
mockbooleanNoEnable mock mode for testing without a relay (default: false)
auditbooleanNoEnable tamper-evident audit logging (default: false). See Audit Logging

Use environment variables instead of hardcoding keys:

const mgr = new SkytaleChannelManager({
identity: "my-agent",
apiKey: process.env.SKYTALE_API_KEY,
apiUrl: "https://api.skytale.sh",
});

create(channelName: string): Promise<void>

Section titled “create(channelName: string): Promise<void>”

Create a new encrypted channel. The caller becomes the first member of the MLS group.

await mgr.create("myorg/team/general");

Parameters:

  • channelName — Channel name in org/namespace/service format

Throws: ChannelError if the channel name is invalid. MlsError if MLS group creation fails.


invite(channelName: string, maxUses?: number, ttl?: number): Promise<string>

Section titled “invite(channelName: string, maxUses?: number, ttl?: number): Promise<string>”

Create an invite token for a channel. The returned skt_inv_... token can be shared with other agents who call joinWithToken() to join.

const token = await mgr.invite("myorg/team/general");
// Share token with the joining agent

Parameters:

  • channelName — Name of the channel to invite to
  • maxUses — Maximum number of times the token can be used (default: 1)
  • ttl — Token lifetime in seconds (default: 3600 = 1 hour)

Returns: Invite token string (e.g. "skt_inv_...")


joinWithToken(channelName: string, token: string, timeout?: number): Promise<void>

Section titled “joinWithToken(channelName: string, token: string, timeout?: number): Promise<void>”

Join a channel using an invite token. The MLS key exchange is handled automatically via the API server.

await mgr.joinWithToken("myorg/team/general", token);

Parameters:

  • channelName — Name of the channel to join
  • token — Invite token from invite()
  • timeout — Maximum time to wait for key exchange in ms (default: 60000)

Throws: AuthError if the token is invalid or expired. MlsError if the key exchange fails.


send(channelName: string, message: string | Buffer): void

Section titled “send(channelName: string, message: string | Buffer): void”

Send an encrypted message to all channel members. Strings are UTF-8 encoded automatically.

mgr.send("myorg/team/general", "Hello, agents!");

receive(channelName: string, timeout?: number): Promise<string[]>

Section titled “receive(channelName: string, timeout?: number): Promise<string[]>”

Drain all buffered messages. Waits up to timeout ms (default: 5000) if the buffer is empty.

const msgs = await mgr.receive("myorg/team/general");
for (const msg of msgs) {
console.log("Received:", msg);
}

receiveLatest(channelName: string, timeout?: number): Promise<string | null>

Section titled “receiveLatest(channelName: string, timeout?: number): Promise<string | null>”

Return only the most recent message, discarding older ones.

const latest = await mgr.receiveLatest("myorg/team/general");
if (latest) {
console.log("Latest:", latest);
}

sendEnvelope(channelName: string, envelope: Envelope): void

Section titled “sendEnvelope(channelName: string, envelope: Envelope): void”

Send a structured envelope on a channel. The envelope is serialized and sent through the MLS-encrypted channel.

import { Protocol, type Envelope } from "@skytalesh/sdk";
mgr.sendEnvelope("org/ns/chan", {
protocol: Protocol.A2A,
contentType: "application/json",
payload: Buffer.from('{"parts":[]}'),
});

receiveEnvelopes(channelName: string, timeout?: number): Promise<Envelope[]>

Section titled “receiveEnvelopes(channelName: string, timeout?: number): Promise<Envelope[]>”

Receive structured envelopes. Messages that aren’t valid envelopes (e.g. raw strings from send()) are auto-wrapped as Protocol.RAW.

const envelopes = await mgr.receiveEnvelopes("org/ns/chan");
for (const e of envelopes) {
console.log(e.protocol, e.payload.toString());
}

Return names of all active channels.

const channels = mgr.listChannels();
console.log("Active channels:", channels);

Stop all background listener threads and release resources.

mgr.close();

Event emitter for real-time message handling. Fires for every incoming message on any channel.

mgr.on("message", (channelName: string, message: Buffer) => {
console.log(`[${channelName}] ${message.toString()}`);
});

Generate an MLS key package for joining a channel via the manual flow.


addMember(channelName: string, keyPackage: Buffer): Buffer

Section titled “addMember(channelName: string, keyPackage: Buffer): Buffer”

Add a member to a channel. Returns the MLS Welcome message.



Protocol-tagged messages for multi-protocol channels.

import { Protocol, type Envelope, serializeEnvelope, deserializeEnvelope } from "@skytalesh/sdk";

Supported wire protocols:

ValueDescription
Protocol.RAWPlain bytes (default, backward compatible)
Protocol.A2AGoogle Agent-to-Agent protocol
Protocol.MCPModel Context Protocol
Protocol.SLIMSLIM (Secure Lightweight Inter-agent Messaging)
Protocol.ACPIBM Agent Communication Protocol
Protocol.ANPAgent Network Protocol (DID-based)
Protocol.LMOSEclipse LMOS (Web of Things)
Protocol.NLIPNatural Language Interaction Protocol
interface Envelope {
protocol: Protocol;
contentType: string;
payload: Buffer;
metadata?: Record<string, unknown>;
}
FieldTypeDescription
protocolProtocolSource protocol identifier
contentTypestringMIME type (e.g. "application/json")
payloadBufferRaw payload bytes
metadataRecord<string, unknown>Optional key-value metadata

Serialize to wire format: [header_len:4 LE][json header][payload].

deserializeEnvelope(data: Buffer): Envelope

Section titled “deserializeEnvelope(data: Buffer): Envelope”

Deserialize from wire format. Throws on invalid data.

const env: Envelope = {
protocol: Protocol.A2A,
contentType: "application/json",
payload: Buffer.from('{"parts":[]}'),
};
const data = serializeEnvelope(env);
const env2 = deserializeEnvelope(data);
console.log(env2.protocol); // "a2a"

Expose Skytale encrypted channels as MCP tools for Claude Desktop, Cursor, or any MCP-compatible client.

Terminal window
npx @skytalesh/mcp-server
ToolDescription
skytale_createCreate an encrypted channel
skytale_inviteGenerate an invite token for a channel
skytale_joinJoin a channel with an invite token
skytale_sendSend a message on a channel
skytale_receiveReceive messages from a channel
skytale_channelsList active channels

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
"mcpServers": {
"skytale": {
"command": "npx",
"args": ["@skytalesh/mcp-server"],
"env": {
"SKYTALE_API_KEY": "sk_live_..."
}
}
}
}

Add to .cursor/mcp.json:

{
"mcpServers": {
"skytale": {
"command": "npx",
"args": ["@skytalesh/mcp-server"]
}
}
}
import { createServer } from "@skytalesh/mcp-server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const { server } = createServer({
// Reads SKYTALE_IDENTITY, SKYTALE_API_KEY, SKYTALE_RELAY from environment
});
const transport = new StdioServerTransport();
await server.connect(transport);

Use Skytale as the transport layer for MCP protocol messages, replacing plaintext HTTP/stdio with MLS-encrypted channels.

import { SkytaleChannelManager } from "@skytalesh/sdk";
import { SkytaleTransport } from "@skytalesh/sdk/mcp-transport";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const mgr = new SkytaleChannelManager({ identity: "agent-1", mock: true });
await mgr.create("org/ns/mcp-rpc");
const transport = new SkytaleTransport(mgr, "org/ns/mcp-rpc");
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);

Expose Skytale channels as Mastra tools with Zod-validated schemas.

Terminal window
npm install @skytalesh/sdk @mastra/core zod
import { Agent } from "@mastra/core";
import { skytaleTools } from "@skytalesh/sdk/mastra";
import { SkytaleChannelManager } from "@skytalesh/sdk";
const mgr = new SkytaleChannelManager({ identity: "my-agent" });
const tools = skytaleTools(mgr);
const agent = new Agent({ tools });

The skytaleTools(manager) function returns an object with three tools:

Tool IDDescription
skytaleSendSend an encrypted message to a Skytale channel
skytaleReceiveReceive encrypted messages from a Skytale channel
skytaleChannelsList all active Skytale encrypted channels

Each tool uses createTool() from @mastra/core with Zod input/output schemas for runtime validation.


Set mock: true to test your agent logic without a running relay:

const mgr = new SkytaleChannelManager({ identity: "test-agent", mock: true });
await mgr.create("org/ns/test");
mgr.send("org/ns/test", "hello");
const msgs = await mgr.receive("org/ns/test"); // ["hello"]
mgr.close();

Mock mode is useful for unit tests, CI pipelines, and local development. All operations work identically to production, including envelopes. Also enabled by setting SKYTALE_MOCK=1 environment variable.


The TypeScript SDK includes the same agent security modules as the Python SDK: identity management, audit logging, attestations, trust circles, and agent registry.

import { AgentIdentity } from "@skytalesh/sdk/identity";
// Generate a new Ed25519 DID:key identity
const identity = AgentIdentity.generate();
console.log(identity.did); // did:key:z6Mk...
console.log(identity.publicKey); // Buffer (32 bytes)
console.log(identity.privateKey); // Buffer (32 bytes)
// Restore from an existing private key
const restored = AgentIdentity.fromPrivateKey(savedKeyBuffer);
// Parse a DID (public key only, cannot sign)
const peer = AgentIdentity.fromDid("did:key:z6Mk...");
// Sign and verify
const signature = identity.sign(Buffer.from("message"));
const valid = identity.verify(Buffer.from("message"), signature);
// Generate a W3C DID Document
const doc = identity.toDidDocument();
Method / PropertyDescription
AgentIdentity.generate()Generate a new Ed25519 key pair and did:key DID
AgentIdentity.fromPrivateKey(privateKey: Buffer)Restore from 32-byte Ed25519 private key
AgentIdentity.fromDid(did: string)Parse a did:key URI (public key only)
.didDID URI string
.publicKeyEd25519 public key (Buffer, 32 bytes)
.privateKeyEd25519 private key (Buffer | null)
.sign(message: Buffer): BufferSign a message (64-byte Ed25519 signature)
.verify(message: Buffer, signature: Buffer): booleanVerify a signature
.toDidDocument(): Record<string, unknown>Generate a W3C DID Document
import { AuditLog, AuditEvent, EventType } from "@skytalesh/sdk/audit";
import { SkytaleChannelManager } from "@skytalesh/sdk";
// Enable audit logging on the channel manager
const mgr = new SkytaleChannelManager({ identity: "agent", audit: true });
// Events are logged automatically: channelCreated, messageSent, etc.
await mgr.create("acme/ns/svc");
mgr.send("acme/ns/svc", "hello");
const entries = mgr.exportAuditLog();
console.log(`Recorded ${entries.length} events`);
console.log(mgr.verifyAuditChain()); // true
// Standalone usage
const log = new AuditLog("did:key:z6Mk...");
log.record(AuditEvent.channelCreated("acme/ns/svc"));
log.record(AuditEvent.messageSent("acme/ns/svc", 256));
console.log(log.verifyChain()); // true
Class / MethodDescription
new AuditLog(agentDid?)Create a standalone audit log
log.record(event)Record an event, returns AuditEntry
log.verifyChain(): booleanVerify hash chain integrity
log.exportChain()Export as serializable objects
log.exportJson(): stringExport as JSON string
AuditEvent.channelCreated(channel, agentDid?)Channel creation event
AuditEvent.messageSent(channel, ciphertextLen?)Message send event
AuditEvent.messageReceived(channel, ciphertextLen?, count?)Message receive event
AuditEvent.memberAdded(channel, memberDid?)Member addition event
AuditEvent.channelJoined(channel, agentDid?, method?)Channel join event
AuditEvent.identityVerified(agentDid, method?)Identity verification event
AuditEvent.inviteCreated(channel, maxUses?, ttl?)Invite creation event
import { AgentIdentity } from "@skytalesh/sdk/identity";
import {
createAttestation,
verifyAttestation,
disclosedClaims,
serializeAttestation,
} from "@skytalesh/sdk/attestation";
const issuer = AgentIdentity.generate();
// Create an attestation with selective disclosure
const att = createAttestation(
issuer,
"did:key:z6MkTarget...",
{ task_score: 0.95, task_type: "analysis", internal_id: "x-789" },
{ disclosed: ["task_score", "task_type"], ttl: 3600 },
);
// Verify signature and expiration
const valid = verifyAttestation(att, issuer.publicKey); // true
// Get only the disclosed claims (undisclosed appear as SD-JWT hashes)
const visible = disclosedClaims(att);
// { task_score: 0.95, task_type: "analysis", internal_id: "_sd:a1b2c3..." }
// Serialize for channel transmission
const json = serializeAttestation(att);
mgr.send("acme/trust/attestations", json);
FunctionDescription
createAttestation(issuerIdentity, subjectDid, claims, options?)Create a signed SD-JWT attestation. Options: { disclosed?: string[], ttl?: number }
verifyAttestation(attestation, issuerPublicKey: Buffer): booleanVerify signature and expiration
disclosedClaims(attestation): Record<string, unknown>Get disclosed claims (undisclosed as hashes)
serializeAttestation(attestation): stringSerialize to JSON for transmission
import { TrustCircle, type AdmissionPolicy } from "@skytalesh/sdk/trust";
const policy: AdmissionPolicy = {
requiredClaims: ["task_score", "certification"],
minScore: 0.8,
requiredIssuers: ["did:key:z6MkTrustedIssuer..."],
minAttestations: 1,
};
const circle = await TrustCircle.create(mgr, "acme/trusted/agents", policy);
// Evaluate attestations before admitting
const [passed, reasons] = circle.evaluatePolicy([att1, att2]);
// Admit an agent
const admitted = circle.requestAdmission("did:key:z6Mk...", [att], keyPackageBytes);
console.log(circle.members()); // ["did:key:z6Mk..."]
// Revoke membership
circle.revoke("did:key:z6Mk...");
// Access the policy
console.log(circle.policy.requiredClaims);
Class / MethodDescription
TrustCircle.create(manager, channel, policy): Promise<TrustCircle>Create a trust circle with a new channel
circle.evaluatePolicy(attestations): [boolean, string[]]Evaluate attestations against the policy
circle.requestAdmission(requesterDid, attestations, keyPackage): booleanRequest admission (admits if policy passes)
circle.members(): string[]Sorted list of member DIDs
circle.revoke(did: string): voidRevoke a member’s membership
circle.policyThe AdmissionPolicy for this circle
// Register this agent (requires API key)
const record = await mgr.registerAgent({
capabilities: ["summarization", "translation"],
endpoint: "https://my-agent.example.com/api",
metadata: { version: "1.0" },
});
// Search for agents by capability
const agents = await mgr.searchAgents("translation");
for (const agent of agents) {
console.log(agent.did, agent.capabilities);
}
MethodDescription
mgr.registerAgent(options?): Promise<Record<string, unknown>>Register this agent. Options: { capabilities?, endpoint?, metadata? }
mgr.searchAgents(capability?): Promise<Record<string, unknown>[]>Search agents by capability

Commit-reveal auction protocol over MLS-encrypted channels. Agents submit hashed bids, then reveal them — the relay never sees bid values.

import { SealedBidAuction } from "@skytalesh/sdk/auction";
// Types: Commitment, Reveal, AuctionResult

new SealedBidAuction(manager, channel, highestWins?)

Section titled “new SealedBidAuction(manager, channel, highestWins?)”

Create a sealed-bid auction on an encrypted channel.

ParameterTypeDefaultDescription
managerSkytaleChannelManagerChannel manager for messaging
channelstringChannel name for the auction
highestWinsbooleantrueIf true, highest bid wins. Set to false for reverse auctions

auction.commit(bidAmount, metadata?): Commitment

Section titled “auction.commit(bidAmount, metadata?): Commitment”

Create and broadcast a sealed bid commitment. Generates a random salt, hashes the bid, and sends the commitment hash to the channel.

ParameterTypeDefaultDescription
bidAmountnumberThe bid value
metadataRecord<string, unknown>{}Optional metadata (e.g. { asset: "ETH/USDC" })

Returns: A Commitment to use in the reveal phase.

Broadcast the bid reveal for a previous commitment.

Collect messages from the channel (commitments and reveals). Call this between phases to gather messages from other participants. Default timeout: 10000ms.

Resolve the auction and determine the winner. Call after all participants have committed and revealed.

interface Commitment {
bidAmount: number;
salt: Buffer;
commitmentHash: string;
metadata: Record<string, unknown>;
timestamp: number;
}
interface Reveal {
sender: string;
bidAmount: number;
saltHex: string;
commitmentHash: string;
metadata: Record<string, unknown>;
valid: boolean;
}
interface AuctionResult {
winner: string | null;
winningBid: number;
reveals: Reveal[];
invalidReveals: Reveal[];
}
import { SkytaleChannelManager } from "@skytalesh/sdk";
import { SealedBidAuction } from "@skytalesh/sdk/auction";
const mgr = new SkytaleChannelManager({ identity: "bidder", apiKey: "sk_live_..." });
await mgr.create("fund/auctions/lot-42");
const auction = new SealedBidAuction(mgr, "fund/auctions/lot-42");
// Phase 1: Commit — each agent submits a hashed bid
const commitment = auction.commit(1500, { asset: "ETH/USDC" });
// Collect other participants' commitments
await auction.collect(15_000);
// Phase 2: Reveal — each agent reveals their bid
auction.reveal(commitment);
// Collect other participants' reveals
await auction.collect(15_000);
// Phase 3: Resolve — determine the winner
const result = auction.resolve();
console.log(`Winner: ${result.winner} with bid ${result.winningBid}`);
mgr.close();

EIP-712 payment authorization for pay-per-use agent services. When the API returns HTTP 402, the SDK can automatically sign and retry with a payment signature.

import { PaymentConfig } from "@skytalesh/sdk/payment";
// Types: PaymentOptions, PaymentRequirement, SignedPayment
FieldTypeDefaultDescription
walletKeystringHex-encoded private key (with or without 0x prefix)
networkstring"base-sepolia"Blockchain network for settlement
maxPerMessagestring"0.01"Maximum USDC to pay per operation
autoPaybooleantrueAuto-sign and retry on 402. If false, throws an error

Supported networks: base-sepolia, base-mainnet, avalanche-mainnet, avalanche-fuji, ethereum-mainnet, ethereum-sepolia.

PaymentConfig.parseRequirement(headerValue): PaymentRequirement

Section titled “PaymentConfig.parseRequirement(headerValue): PaymentRequirement”

Static method. Parse a base64-encoded PAYMENT-REQUIRED header from a 402 response.

config.signPayment(requirement): Promise<SignedPayment>

Section titled “config.signPayment(requirement): Promise<SignedPayment>”

Sign an x402 payment authorization using EIP-712. Requires ethers as a peer dependency.

Returns: SignedPayment with a base64-encoded signature for the PAYMENT-SIGNATURE header.

Throws: Error if the required amount exceeds maxPerMessage.

import { PaymentConfig } from "@skytalesh/sdk/payment";
const config = new PaymentConfig({
walletKey: "0xabc123...",
network: "base-sepolia",
maxPerMessage: "0.01",
});
// When you receive a 402 response:
const requirement = PaymentConfig.parseRequirement(
response.headers.get("PAYMENT-REQUIRED")!,
);
const signed = await config.signPayment(requirement);
// Retry with the payment signature
const retry = await fetch(url, {
headers: { "PAYMENT-SIGNATURE": signed.signature },
});

All SDK methods throw typed exceptions that extend SkytaleError. Each exception carries structured attributes for programmatic handling:

ExceptionWhen
AuthErrorAPI key invalid, expired, or missing
TransportErrorRelay unreachable, connection error, timeout
ChannelErrorInvalid channel name, channel not found
MlsErrorBad key package, invalid Welcome, decryption failure
QuotaExceededErrorFree tier message limit reached

Every exception includes:

  • code — machine-readable error code (e.g. "auth_failed")
  • httpStatus — HTTP status code when applicable (e.g. 401)
  • docUrl — link to relevant documentation
import { SkytaleChannelManager, SkytaleError, AuthError, ChannelError } from "@skytalesh/sdk";
try {
const mgr = new SkytaleChannelManager({ identity: "agent" });
await mgr.create("bad-name");
} catch (e) {
if (e instanceof AuthError) {
console.log(`Auth failed (HTTP ${e.httpStatus}): ${e.message}`);
console.log(`Fix: ${e.docUrl}`);
} else if (e instanceof ChannelError) {
console.log(`Channel error [${e.code}]: ${e.message}`);
} else if (e instanceof SkytaleError) {
console.log(`SDK error: ${e.message}`);
}
}

VariableDescriptionDefault
SKYTALE_RELAYRelay server URLhttps://relay.skytale.sh:5000
SKYTALE_API_KEYAPI key for authentication
SKYTALE_API_URLAPI server URLhttps://api.skytale.sh
SKYTALE_DATA_DIRMLS state directoryAuto-generated
SKYTALE_IDENTITYDefault agent identity
SKYTALE_MOCKEnable mock mode (1, true, yes)false

import { SkytaleChannelManager } from "@skytalesh/sdk";
async function main() {
// Agent A: create a channel and generate an invite
const alice = new SkytaleChannelManager({ identity: "alice" });
await alice.create("myorg/team/general");
const token = await alice.invite("myorg/team/general");
// Agent B: join with the invite token
const bob = new SkytaleChannelManager({ identity: "bob" });
await bob.joinWithToken("myorg/team/general", token);
// Send and receive — end-to-end encrypted
alice.send("myorg/team/general", "Hello from Alice!");
alice.send("myorg/team/general", "Another message!");
const msgs = await bob.receive("myorg/team/general");
for (const msg of msgs) {
console.log(`Bob received: ${msg}`);
}
// Clean up
alice.close();
bob.close();
}
main().catch(console.error);