Connect your app to the Teneo AI Agent Network
The Teneo Protocol SDK lets you connect your application to a decentralized network of specialized AI agents. Instead of calling a single AI model, your app taps into an entire ecosystem where:
- 🤖 Multiple AI agents with different specializations handle your requests
- 🧠 Intelligent routing automatically selects the best agent for each query
- 🔐 Flexible authentication — Ethereum wallet signatures or access key
The SDK now automatically detects when an agent is missing from your room and adds it before sending your command — no more failed requests or retry cycles:
const sdk = new TeneoSDK({
wsUrl: process.env.WS_URL,
privateKey: process.env.PRIVATE_KEY,
autoSummon: true // Enable auto-summon
});
// Agent not in room? SDK handles it automatically:
// 1. Checks local cache → agent missing
// 2. Fires autosummon:start
// 3. Adds agent to room
// 4. Fires autosummon:success
// 5. Sends your command
await sdk.sendDirectCommand({
agent: "example-agent",
command: "latest 2h",
room: roomId
}, true);Lifecycle events for full visibility:
autosummon:start— agent addition initiatedautosummon:success— agent added, command proceedingautosummon:failed— agent not found or addition failed, falls back to coordinator
Constructor now validates private key format immediately, catching empty or malformed keys before they cause cryptic signing errors downstream.
Version 3.0 introduces API Naming Improvements for better clarity and consistency:
All method names have been improved to be more explicit and intuitive:
- Room subscriptions -
subscribeToPublicRoom()makes it clear these only work for public rooms - Cache-only methods -
getCached*()prefix makes sync vs async operations obvious - Boolean semantics -
checkAgentInRoom()clearly indicates it may returnundefined - Search scope -
findAvailableAgentsBy*()clarifies network-wide search
All old method names still work! They're deprecated with helpful aliases:
- Old names forward to new implementations
- TypeScript shows deprecation warnings
- Migrate at your convenience
Jump to Migration Guide | See Full CHANGELOG
pnpm install @teneo-protocol/sdkimport { TeneoSDK, SDKConfigBuilder } from "@teneo-protocol/sdk";
// Option A: Initialize with your Ethereum private key
const sdk = new TeneoSDK({
wsUrl: "wss://backend.developer.chatroom.teneo-protocol.ai/ws",
privateKey: "0x..." // Your private key with 0x prefix
});
// Option B: Initialize with an access key (no private key needed)
// const config = new SDKConfigBuilder()
// .withWebSocketUrl("wss://backend.developer.chatroom.teneo-protocol.ai/ws")
// .withAccessKey("sk_live_...", "0xYourWalletAddress")
// .withNetwork("peaq")
// .build();
// const sdk = new TeneoSDK(config);
// 2. Listen for responses
sdk.on("agent:response", (response) => {
console.log(`${response.agentName}: ${response.humanized}`);
});
// 3. Connect (authenticates automatically)
await sdk.connect();
// 4. Get your private rooms (auto-available after auth)
const rooms = sdk.getRooms();
const roomId = rooms[0].id;
// 5. Send a message to a room
// WITH COORDINATOR (when available):
await sdk.sendMessage("Give me the last 5 tweets from @elonmusk", {
room: roomId
});
// → Coordinator selects best agent automatically
// WITHOUT COORDINATOR (direct command required):
await sdk.sendMessage("@X Platform Agent search bitcoin 5", {
room: roomId
});
// → Direct command to specific agent by nameThat's it! After authentication:
- Your private rooms are automatically available
- Send messages to any room by ID
- With coordinator: Use natural language - coordinator routes to best agent
- Without coordinator: Use
@Agent Name command paramssyntax to target specific agents - Responses arrive via the event listener
The SDK supports two routing approaches depending on your environment:
graph TB
UserMessage[User Message]
CheckEnv{Environment Has<br/>Coordinator?}
CoordPath[Coordinator Routing]
DirectPath[Direct Routing]
CoordSelect[Coordinator Selects<br/>Best Agent]
DirectTarget[Direct to<br/>Named Agent]
Agent[Agent Processes<br/>Request]
UserMessage --> CheckEnv
CheckEnv -->|Yes| CoordPath
CheckEnv -->|No| DirectPath
CoordPath -->|"Natural language<br/>supported"| CoordSelect
DirectPath -->|"@Agent Name required"| DirectTarget
CoordSelect --> Agent
DirectTarget --> Agent
Key Differences:
- Coordinator environments: Both natural language and direct commands work
- Non-coordinator environments: Must use
@Agent Name command paramssyntax
Your App
↓
Teneo SDK (This library)
↓
WebSocket Connection
↓
Teneo Coordinator ──→ Selects best agent
↓
┌─────────┬─────────┬─────────┬─────────┐
│ X │Analytics│ Reddit │ Custom │
│ Agent │ Agent │ Agent │ Agents │
└─────────┴─────────┴─────────┴─────────┘
The SDK supports two authentication methods:
// Challenge-response authentication flow:
// 1. SDK connects to Teneo network
// 2. Server sends random challenge string
// 3. SDK signs: "Teneo authentication challenge: {challenge}"
// 4. Server verifies signature against your wallet address
// 5. ✅ Authenticated! You can now send messages
// Your private key never leaves your machine// Access-key authentication — no private key needed:
// 1. SDK connects to Teneo network
// 2. SDK sends access key + wallet address
// 3. Server validates the key and authenticates
// 4. ✅ Authenticated! Payments are handled server-side
const config = new SDKConfigBuilder()
.withWebSocketUrl(WS_URL)
.withAccessKey("sk_live_...", "0xYourWalletAddress")
.withNetwork("peaq")
.build();Access key auth is ideal for server-side integrations where you don't want to manage private keys directly. The backend handles payment signing using the stored session key.
git clone https://github.com/TeneoProtocolAI/teneo-sdk.git
cd teneo-sdk
pnpm install
pnpm run build
# Set credentials
export PRIVATE_KEY=your_private_key
export TENEO_WS_URL=wss://backend.developer.chatroom.teneo-protocol.ai/wsThe Production Dashboard is a comprehensive example showcasing ALL SDK features in a real-world web application:
pnpm example:dashboard
# or
cd examples/production-dashboard && bun run server.tsThen open: http://localhost:3000
What it demonstrates:
- ✅ Full WebSocket Integration - Connection, authentication, auto-reconnection
- ✅ Room Management (v2.0) - Create, update, delete rooms with ownership tracking
- ✅ Agent-Room Management (v2.0) - Add/remove agents, list room agents with caching
- ✅ Message Sending - Coordinator-based and direct agent commands
- ✅ Real-time Updates - Server-Sent Events (SSE) for live dashboard
- ✅ Agent Discovery - List agents with capabilities and status
- ✅ Secure Private Key Handling - AES-256-GCM encryption in memory
- ✅ Webhook Integration - Real-time event streaming with circuit breaker
- ✅ Health Monitoring -
/healthand/metricsendpoints - ✅ Complete Event System - All SDK events with real-time UI updates
Built with Hono (fast web framework) and Bun (fast JavaScript runtime). See examples/production-dashboard/README.md for details.
Wait for specific responses with timeout:
const sdk = new TeneoSDK({
wsUrl: process.env.TENEO_WS_URL!,
privateKey: process.env.PRIVATE_KEY!
});
await sdk.connect();
// Wait for response (blocks until agent responds or timeout)
const response = await sdk.sendMessage("Give me the last 5 tweets from @elonmusk?", {
waitForResponse: true,
timeout: 30000, // 30 seconds
format: "both" // Get both raw data and humanized text
});
console.log("Agent:", response.agentName);
console.log("Answer:", response.humanized);
console.log("Raw data:", response.raw);
// Output:
// Agent: X Agent
// Answer: Timeline for @elonmusk (5 tweets) ...Organize agents by context using rooms:
const sdk = new TeneoSDK({
wsUrl: process.env.TENEO_WS_URL!,
privateKey: process.env.PRIVATE_KEY!,
autoJoinPublicRooms: ["crawler-room-id", "kol-tracker-room-id"] // public rooms only
});
// Each room may have different agents available
// Note: Private rooms are automatically available after auth
await sdk.connect();
// Send to specific room contexts
// WITH COORDINATOR:
await sdk.sendMessage("Get latest tweets from @elonmusk", { room: "kol-tracker-room-id" });
// → Coordinator routes to best agent in room
await sdk.sendMessage("Crawl this website for data", { room: "crawler-room-id" });
// → Coordinator routes to best agent in room
// WITHOUT COORDINATOR (direct commands required):
await sdk.sendMessage("@X Platform Agent timeline @elonmusk 5", { room: "kol-tracker-room-id" });
// → Direct to X Platform Agent in room
await sdk.sendMessage("@Web Crawler Agent crawl https://example.com", { room: "crawler-room-id" });
// → Direct to Web Crawler Agent in room
// Manage rooms dynamically
const rooms = sdk.getSubscribedRooms();
console.log("Active rooms:", rooms);
// Output: Active rooms: ['crawler-room-id', 'kol-tracker-room-id']Receive agent responses via HTTP POST to your server:
// Your webhook endpoint (Express)
import express from "express";
const app = express();
app.use(express.json());
app.post("/teneo-webhook", (req, res) => {
const { event, data, timestamp } = req.body;
if (event === "task_response") {
console.log(`Agent: ${data.agentName}`);
console.log(`Message: ${data.content}`);
// Save to your database
db.saveAgentResponse({
agentId: data.agentId,
content: data.content,
timestamp: new Date(timestamp)
});
}
res.sendStatus(200);
});
app.listen(8080);
// Teneo SDK with webhook
const sdk = new TeneoSDK({
wsUrl: process.env.TENEO_WS_URL!,
privateKey: process.env.PRIVATE_KEY!,
webhookUrl: "https://your-webhook.com/",
webhookHeaders: {
Authorization: "Bearer your-secret-token"
}
});
// Monitor webhook delivery
sdk.on("webhook:sent", () => console.log("📤 Webhook sent"));
sdk.on("webhook:success", () => console.log("✅ Webhook delivered"));
sdk.on("webhook:error", (error) => {
console.error("❌ Webhook failed:", error.message);
// Circuit breaker will automatically retry
});
await sdk.connect();
// Check webhook health
const status = sdk.getWebhookStatus();
console.log("Pending:", status.queue.pending);
console.log("Circuit state:", status.queue.circuitState); // OPEN/CLOSED/HALF_OPENThe Teneo SDK supports two deployment environments via different WebSocket endpoints:
- URL:
wss://backend.developer.chatroom.teneo-protocol.ai/ws - Whitelist: Not required - open for testing
- Use case: Development, testing, and experimentation
Configuration:
# .env file
TENEO_WS_URL=wss://backend.developer.chatroom.teneo-protocol.ai/ws- URL:
wss://backend.chatroom.teneo-protocol.ai/ws - Whitelist: Required - you must be whitelisted to use this endpoint
- Use case: Production applications and B2B integrations
- Get access: Request whitelist access at https://teneo-protocol.ai/data-access
Configuration:
# .env file
TENEO_WS_URL=wss://backend.chatroom.teneo-protocol.ai/wsNote: The SDK uses the development endpoint by default. For production use, you need to be whitelisted and explicitly configure the production URL. Use the development endpoint for testing without restrictions.
Create and manage multiple rooms for different contexts and use cases.
// Create a new room
const room = await sdk.createRoom({
name: "Crypto Research",
description: "Room for crypto analysis"
});
console.log(`Created room: ${room.id}`);
// Check if you can create more rooms
if (sdk.canCreateRoom()) {
await sdk.createRoom({ name: "Gaming Room" });
} else {
console.log(`Room limit reached: ${sdk.getOwnedRoomCount()}/${sdk.getRoomLimit()}`);
}// Get all owned rooms (rooms you created)
const ownedRooms = sdk.getOwnedRooms();
console.log(`You own ${ownedRooms.length} rooms:`);
ownedRooms.forEach((room) => {
console.log(` - ${room.name} (${room.is_public ? "Public" : "Private"})`);
});
// Get shared rooms (rooms you were invited to)
const sharedRooms = sdk.getSharedRooms();
console.log(`You have access to ${sharedRooms.length} shared rooms`);
// Get all rooms (owned + shared)
const allRooms = sdk.getAllRooms();
console.log(`Total rooms accessible: ${allRooms.length}`);
// Get specific room by ID
const room = sdk.getRoom("room-123");
if (room) {
console.log(`Room: ${room.name}`);
console.log(`Created by: ${room.created_by}`);
console.log(`You are ${room.is_owner ? "owner" : "member"}`);
}
// Check room limits
console.log(`Room capacity: ${sdk.getOwnedRoomCount()}/${sdk.getRoomLimit()}`);// Update room details (owner only)
const updated = await sdk.updateRoom("room-123", {
name: "Updated Room Name",
description: "New description"
});
console.log(`Room updated: ${updated.name}`);// Delete a room you own
await sdk.deleteRoom("room-123");
console.log("Room deleted");
// Listen for deletion events
sdk.on("room:deleted", (roomId) => {
console.log(`Room ${roomId} was deleted`);
});// Room lifecycle events
sdk.on("room:created", (room) => {
console.log(`✅ Created: ${room.name}`);
});
sdk.on("room:updated", (room) => {
console.log(`📝 Updated: ${room.name}`);
});
sdk.on("room:deleted", (roomId) => {
console.log(`🗑️ Deleted: ${roomId}`);
});
// Error handling
sdk.on("room:create_error", (error) => {
console.error(`Failed to create room: ${error.message}`);
});
sdk.on("room:update_error", (error, roomId) => {
console.error(`Failed to update room ${roomId}: ${error.message}`);
});
sdk.on("room:delete_error", (error, roomId) => {
console.error(`Failed to delete room ${roomId}: ${error.message}`);
});Customize which agents are available in each of your rooms.
// Add an agent to your room (owner only)
await sdk.addAgentToRoom("room-123", "agent-456");
console.log("Agent added to room");
// Listen for confirmation
sdk.on("agent_room:agent_added", (roomId, agentId) => {
console.log(`✅ Agent ${agentId} added to room ${roomId}`);
});// Remove an agent from your room (owner only)
await sdk.removeAgentFromRoom("room-123", "agent-456");
console.log("Agent removed from room");
// Listen for confirmation
sdk.on("agent_room:agent_removed", (roomId, agentId) => {
console.log(`🗑️ Agent ${agentId} removed from room ${roomId}`);
});// List all agents in a room (with 5-minute cache)
const roomAgents = await sdk.listRoomAgents("room-123");
console.log(`${roomAgents.length} agents in this room:`);
roomAgents.forEach((agent) => {
console.log(` - ${agent.agent_name} (${agent.status})`);
if (agent.capabilities) {
console.log(` Capabilities: ${agent.capabilities.map((c) => c.name).join(", ")}`);
}
});
// Force refresh (bypass cache)
const freshAgents = await sdk.listRoomAgents("room-123", false);// List agents NOT yet in the room (available to add)
const available = await sdk.listAvailableAgents("room-123");
console.log(`${available.length} agents available to add:`);
available.forEach((agent) => {
console.log(` - ${agent.agent_name}`);
if (agent.description) {
console.log(` ${agent.description}`);
}
});// Check if specific agent is in room (instant, no network call)
const inRoom = sdk.checkAgentInRoom("room-123", "agent-456");
if (inRoom === true) {
console.log("Agent is in the room");
} else if (inRoom === false) {
console.log("Agent is NOT in the room");
} else {
console.log("Cache not available - call listRoomAgents() first");
}
// Get room agent count (instant)
const count = sdk.getCachedRoomAgentCount("room-123");
if (count !== undefined) {
console.log(`Room has ${count} agents`);
}
// Get cached room agents (instant)
const cached = sdk.getCachedRoomAgents("room-123");
if (cached) {
console.log("Agents:", cached.map((a) => a.agent_name).join(", "));
} else {
console.log("No cached data - call listRoomAgents() first");
}
// Get cached available agents (instant)
const cachedAvailable = sdk.getCachedAvailableAgents("room-123");// Caching behavior:
// - listRoomAgents() and listAvailableAgents() cache results for 5 minutes
// - Cache automatically invalidated when you add/remove agents
// - Cache automatically invalidated on agent status updates
// Manual cache invalidation (if needed)
sdk.invalidateAgentRoomCache("room-123");
console.log("Cache cleared for room-123");// Agent add/remove events
sdk.on("agent_room:agent_added", (roomId, agentId) => {
console.log(`✅ Agent ${agentId} added to ${roomId}`);
});
sdk.on("agent_room:agent_removed", (roomId, agentId) => {
console.log(`🗑️ Agent ${agentId} removed from ${roomId}`);
});
// List events
sdk.on("agent_room:agents_listed", (roomId, agents) => {
console.log(`📋 ${agents.length} agents in room ${roomId}`);
});
sdk.on("agent_room:available_agents_listed", (agents) => {
console.log(`📋 ${agents.length} agents available to add`);
});
// Status updates (real-time)
sdk.on("agent_room:status_update", (data) => {
console.log(`🔄 Agent ${data.agentId} status updated in room ${data.roomId}`);
console.log(` New status: ${data.status}`);
});
// Error handling
sdk.on("agent_room:add_error", (error, roomId) => {
console.error(`Failed to add agent to ${roomId}: ${error.message}`);
});
sdk.on("agent_room:remove_error", (error, roomId) => {
console.error(`Failed to remove agent from ${roomId}: ${error.message}`);
});
sdk.on("agent_room:list_error", (error, roomId) => {
console.error(`Failed to list agents in ${roomId}: ${error.message}`);
});
sdk.on("agent_room:list_available_error", (error) => {
console.error(`Failed to list available agents: ${error.message}`);
});When you send a command to an agent that isn't in your room yet, the SDK can automatically add it for you. No manual addAgentToRoom() call needed — just send your command and the SDK handles the rest.
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withAutoSummon(true) // Enable auto-summon
.withNetwork("base")
.build()
);
await sdk.connect();// Just send a command — the agent doesn't need to be in your room
const response = await sdk.sendDirectCommand({
agent: "example-agent",
command: "latest 2h",
room: roomId
}, true);
// Behind the scenes:
// 1. SDK checks if agent is in the room (instant cache lookup)
// 2. If not -> automatically adds the agent to your room
// 3. Sends your command to the agent
// 4. Returns the response
console.log(response.humanized);Track the auto-summon lifecycle to show loading states in your UI:
// Agent is being added to the room
sdk.on("autosummon:start", (agentName, roomId) => {
showLoadingState(`Adding ${agentName} to your room...`);
});
// Agent was successfully added — command is now being processed
sdk.on("autosummon:success", (agentName, agentId, roomId) => {
showNotification(`${agentName} joined your room`);
});
// Agent could not be added (doesn't exist, is offline, etc.)
sdk.on("autosummon:failed", (agentName, roomId, reason) => {
showError(`Could not add ${agentName}: ${reason}`);
});- No duplicates: If the agent is already in the room, the SDK skips the summon and sends your command directly.
- Works with any message method: Auto-summon triggers from both
sendDirectCommand()andsendMessage()when the message targets an@agent. - Graceful fallback: If the local cache is empty (e.g., first connection), the SDK falls back to server-side detection — your command still works, just with a small extra round trip.
- Room ownership required: Auto-summon only works in rooms you own, since adding agents requires owner permissions.
// Complete workflow: Create room and customize agents
async function setupCustomRoom() {
// 1. Create a new room
const room = await sdk.createRoom({
name: "My Custom Room",
description: "Specialized agent room"
});
console.log(`✅ Created room: ${room.id}`);
// 2. See what agents are available
const available = await sdk.listAvailableAgents(room.id);
console.log(`${available.length} agents available`);
// 3. Add specific agents you want
const cryptoAgent = available.find((a) => a.agent_name?.includes("Crypto"));
if (cryptoAgent) {
await sdk.addAgentToRoom(room.id, cryptoAgent.agent_id);
console.log(`✅ Added ${cryptoAgent.agent_name}`);
}
const analyticsAgent = available.find((a) => a.agent_name?.includes("Analytics"));
if (analyticsAgent) {
await sdk.addAgentToRoom(room.id, analyticsAgent.agent_id);
console.log(`✅ Added ${analyticsAgent.agent_name}`);
}
// 4. Verify room configuration
const roomAgents = await sdk.listRoomAgents(room.id);
console.log(`\n🎯 Room configured with ${roomAgents.length} agents:`);
roomAgents.forEach((agent) => {
console.log(` - ${agent.agent_name}`);
});
// 5. Now send messages to this room
await sdk.sendMessage("Analyze BTC price trends", { room: room.id });
}
setupCustomRoom();The SDK supports automatic micropayments to AI agents using the x402 protocol.
import { TeneoSDK } from "@teneo-protocol/sdk";
// Enable payments with auto-approve
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withPayments({
autoApprove: true, // Automatically approve quotes
maxPricePerRequest: 1000000 // Max 1 USDC per request (in micro-units)
})
.build()
);
await sdk.connect();
// Send message - payment handled automatically
const response = await sdk.sendMessage("@X Platform Agent user @elonmusk", {
room: roomId,
waitForResponse: true
});
console.log(response.humanized);
// Output: User profile for @elonmusk...Note: The builder uses
withPayments({ autoApprove: true })while the plain config object usesautoApproveQuotes: true. Both control the same behavior. The builder also acceptsmaxPricePerRequestandquoteTimeout.
For server-side integrations you can authenticate and pay using an access key instead of a private key. The SDK skips x402 payment signing and the backend handles payment signing server-side using the stored session key.
import { TeneoSDK, SDKConfigBuilder } from "@teneo-protocol/sdk";
const config = new SDKConfigBuilder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAccessKey(process.env.ACCESS_KEY!, process.env.WALLET_ADDRESS!)
.withNetwork("peaq")
.build();
const sdk = new TeneoSDK(config);
await sdk.connect();
// Same API as private-key flow — quote approval & confirm_task
// are handled automatically, with the access key instead of x402 signing.
const response = await sdk.sendMessage("@X Platform Agent user @elonmusk", {
room: sdk.getRooms()[0].id,
waitForResponse: true,
timeout: 60_000,
});
console.log(response.humanized);How it works:
withAccessKey(accessKey, walletAddress)replaces challenge-response auth — the SDK sends the access key + wallet address on connect.- On
request_task→task_quote, the SDK auto-confirms (default) and attaches the access key toconfirm_taskinstead of signing an x402 payment header. - No private key is ever loaded into the SDK; all signing happens server-side.
When to use: server-side backends, CI jobs, or any context where managing an Ethereum private key on the client is undesirable.
Full runnable example:
examples/access-key-payment-flow.tsACCESS_KEY=sk_live_... WALLET_ADDRESS=0x... pnpm tsx examples/access-key-payment-flow.ts
The SDK supports USDC payments across multiple EVM networks with dynamic configuration. Network configurations are automatically fetched from the backend during connect(), enabling the protocol to add new networks without requiring SDK updates.
Networks are initialized automatically when you connect:
import { TeneoSDK, NETWORKS, getSupportedNetworks } from "@teneo-protocol/sdk";
const sdk = new TeneoSDK({
wsUrl: "wss://teneo.network/ws",
privateKey: "0x..."
});
// Before connect: NETWORKS is empty
console.log(NETWORKS); // {}
// Networks fetched during connect from backend /api/networks
await sdk.connect();
// After connect: NETWORKS populated with backend configuration
console.log(NETWORKS); // { peaq: {...}, base: {...}, avalanche: {...} }
const networks = getSupportedNetworks(); // ["peaq", "base", "avalanche"]Key Features:
- 🔄 Automatic fetch from backend
/api/networksendpoint duringconnect() - ⚡ 60-minute cache with automatic refresh
- 🛡️ Offline resilience - falls back to cached configs if backend temporarily unavailable
- 🚀 Future-proof - new networks can be added server-side without SDK updates
import {
NETWORKS,
getNetwork,
getDefaultNetwork,
getSupportedNetworks,
isNetworkSupported,
createChainDefinition
} from "@teneo-protocol/sdk";
// Must be called after connect()
await sdk.connect();
// Get all supported networks (dynamically loaded from backend)
const networks = getSupportedNetworks();
console.log(networks); // e.g., ["peaq", "base", "avalanche"]
// Get network by name
const baseNetwork = getNetwork("base");
// Get network by chain ID
const peaqNetwork = getNetwork(3338);
// Get network by CAIP-2 identifier
const avaxNetwork = getNetwork("eip155:43114");
console.log(baseNetwork);
// {
// chainId: 8453,
// name: "Base Mainnet",
// caip2: "eip155:8453",
// rpcUrl: "https://mainnet.base.org",
// usdcContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
// settlementRouter: "0x73fc659Cd5494E69852bE8D9D23FE05Aab14b29B",
// transferHook: "0x081258287F692D61575387ee2a4075f34dd7Aef7",
// eip712: { name: "USD Coin", version: "2" }
// }
// Check if a network is supported
if (isNetworkSupported("base")) {
// Create a viem chain definition
const baseChain = createChainDefinition(baseNetwork);
// Use with PaymentClient or other viem-based operations
}
// Get default network (prefers PEAQ, falls back to first available)
const defaultNetwork = getDefaultNetwork();These networks are currently supported (fetched dynamically from backend):
PEAQ Mainnet (chainId: 3338)
- Original Teneo network
- USDC contract, settlement router, transfer hook configured via backend
Base Mainnet (chainId: 8453)
- Layer 2 scaling solution
- Lower gas fees, faster transactions
Avalanche Mainnet (chainId: 43114)
- High-throughput blockchain
- Sub-second finality
Note: Network configurations are fetched from the backend and may change. Use
getSupportedNetworks()to get the current list. The SDK automatically handles network selection based on agent requirements.
Payment quotes now include settlement router information for enhanced payment routing:
// Request a quote
const quote = await sdk.requestQuote("Analyze this data", "room-id");
// Quote includes settlement router fields (x402 v2.5)
console.log(quote.data.settlement_router); // Router contract address
console.log(quote.data.salt); // Unique transaction salt
console.log(quote.data.facilitator_fee); // Facilitator fee amount
console.log(quote.data.hook); // Transfer hook address
console.log(quote.data.hook_data); // Optional hook data (default: "0x")Note: The SDK automatically handles network selection. The payment server determines which network to use based on the agent's configuration. You don't need to manually configure networks unless you're using the
PaymentClientdirectly for custom payment operations.
You can configure which network to use for all payments in three ways:
Option 1: Using Environment Variable (Global Default)
# Set default network for all payments
export TENEO_NETWORK=base
# Or use chain ID
export TENEO_NETWORK=8453
# Or use CAIP-2 format
export TENEO_NETWORK=eip155:8453import { TeneoSDK } from "@teneo-protocol/sdk";
// SDK automatically uses TENEO_NETWORK env var
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.build()
);
// All payments will use Base network (from TENEO_NETWORK)Option 2: Using Config Builder (Per-SDK Instance)
import { TeneoSDK } from "@teneo-protocol/sdk";
// Configure Base network for this SDK instance
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.withNetwork("base") // Use Base Mainnet
.build()
);
// All payments from this SDK will use Base networkOr using chain ID:
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.withNetworkChainId(43114) // Use Avalanche (chain ID 43114)
.build()
);Network Selection Priority:
The SDK uses this priority order for network selection:
- Per-request network override via
sendDirectCommand({ network: "base" })orsendMessage({ network: "base" }) - Builder
.withNetwork()or.withNetworkChainId()setting TENEO_NETWORKenvironment variable- Default: PEAQ network (eip155:3338)
Example: Per-Request Network Override (Recommended)
Send commands to different chains from a single SDK instance:
const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PRIVATE_KEY!)
.withPayments({ autoApprove: true, maxPricePerRequest: 1000000 })
.build()
);
await sdk.connect();
const roomId = sdk.getRooms()[0].id;
// Pay on PEAQ
await sdk.sendDirectCommand({
agent: "x-agent-enterprise-v2",
command: "user @elonmusk",
room: roomId,
network: "peaq", // Per-request network override
}, true);
// Pay on Base
await sdk.sendDirectCommand({
agent: "x-agent-enterprise-v2",
command: "user @elonmusk",
room: roomId,
network: "base", // Per-request network override
}, true);
// Pay on Avalanche
await sdk.sendDirectCommand({
agent: "x-agent-enterprise-v2",
command: "user @elonmusk",
room: roomId,
network: "avalanche", // Per-request network override
}, true);You can also pass a chain ID instead of a name:
await sdk.sendDirectCommand({
agent: "x-agent-enterprise-v2",
command: "user @elonmusk",
room: roomId,
network: 8453, // Base by chain ID
}, true);The same works with sendMessage:
await sdk.sendMessage("@X Platform Agent user @elonmusk", {
room: roomId,
waitForResponse: true,
network: "avalanche", // Per-request network override
});Example: Multiple SDK Instances with Different Networks
For cases where you want a fixed default per SDK instance:
// Production bot uses PEAQ
const peaqBot = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.PEAQ_PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.withNetwork("peaq")
.build()
);
// Experimental bot uses Base
const baseBot = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.BASE_PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.withNetwork("base")
.build()
);
// High-throughput bot uses Avalanche
const avalancheBot = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(process.env.AVALANCHE_PRIVATE_KEY!)
.withPayments({ autoApprove: true })
.withNetwork("avalanche")
.build()
);Direct agent commands allow you to target a specific agent by name using the @Agent Name command params syntax:
- Required in non-coordinator environments (direct commands are the only way to communicate)
- Optional in coordinator environments (gives you explicit control over which agent handles the request)
// Option 1: Use @mention syntax via sendMessage (recommended)
await sdk.sendMessage("@X Platform Agent search bitcoin 5", {
room: roomId,
waitForResponse: true
});
// Option 2: Use sendDirectCommand for programmatic control
const response = await sdk.sendDirectCommand({
agent: "X Platform Agent",
command: "timeline @elonmusk 5",
room: roomId
}, true); // waitForResponse
console.log(response.humanized);
// Option 3: With per-request network override
const response = await sdk.sendDirectCommand({
agent: "X Platform Agent",
command: "user @elonmusk",
room: roomId,
network: "base", // Per-request network override (peaq, base, avalanche, or chain ID)
}, true);
// Option 4: Fire-and-forget (no wait for response)
await sdk.sendDirectCommand({
agent: "X Platform Agent",
command: "user @elonmusk",
room: roomId
});// IN COORDINATOR ENVIRONMENTS (both work):
// Natural language - coordinator selects best agent
await sdk.sendMessage("Get me the latest tweets from @elonmusk", {
room: roomId,
waitForResponse: true
});
// Direct command - explicit agent selection
await sdk.sendMessage("@X Platform Agent timeline @elonmusk 5", {
room: roomId,
waitForResponse: true
});
// IN NON-COORDINATOR ENVIRONMENTS (direct commands required):
// Direct command - MUST specify agent name
await sdk.sendMessage("@X Platform Agent search bitcoin 5", {
room: roomId,
waitForResponse: true
});
// This will NOT work without coordinator:
// ❌ await sdk.sendMessage("Get bitcoin info", { room: roomId });// Quote received from agent
sdk.on("quote:received", (quote) => {
console.log(`Quote: ${quote.data.pricing.pricePerUnit} USDC`);
console.log(`Agent: ${quote.data.agent_name}`);
console.log(`Expires: ${quote.data.expires_at}`);
});
// Payment attached to request
sdk.on("payment:attached", (data) => {
console.log(`Paid ${data.amount / 1_000_000} USDC to ${data.agentId}`);
});
// Payment blocked (price exceeds maxPricePerRequest)
sdk.on("payment:blocked", (data) => {
console.warn(`Payment blocked: agent ${data.agentId} charges ${data.agentPrice} but max is ${data.maxPrice}`);
});
// Payment errors
sdk.on("payment:error", (error) => {
console.error(`Payment failed: ${error.message}`);
});const sdk = new TeneoSDK(
TeneoSDK.builder()
.withWebSocketUrl("wss://...")
.withAuthentication(privateKey)
.withPayments({
autoApprove: false // Require manual approval
})
.build()
);
// Listen for quotes
sdk.on("quote:received", async (quote) => {
const price = quote.data.pricing.pricePerUnit;
// Check price before approving
if (price <= 0.01) {
// Approve and send payment
await sdk.confirmQuote(quote.data.task_id);
} else {
console.log(`Quote too expensive: $${price}`);
}
});
// Request triggers quote
await sdk.sendMessage("@X Platform Agent user @elonmusk", { room: roomId });Your App Teneo Backend Agent
│ │ │
│──── request_task ──────────>│ │
│ │────── select agent ─────>│
│<───── task_quote ───────────│<────── pricing ──────────│
│ │ │
│ [Auto-approve or manual] │ │
│ │ │
│──── confirm_task ──────────>│ │
│ + x402 payment header │────── execute task ─────>│
│ │ │
│<───── task_response ────────│<────── response ─────────│
│ │ │
│ │──── settle payment ─────>│
└ └ └
Some agents (e.g., Squid Router for cross-chain swaps) need your wallet to sign and submit on-chain transactions. The SDK handles this through a simple event-driven flow.
- Agent requests a transaction — the SDK emits a
wallet:tx_requestedevent with the transaction details - Your app signs and submits — using your wallet/signer of choice (ethers, viem, web3.js, etc.)
- Your app reports the result — call
sendTxResult()so the agent knows what happened - Agent may request more transactions — for multi-step operations (e.g., approve + swap), the agent sends additional
trigger_wallet_txmessages after each result
Your App Teneo Backend Agent
│ │ │
│ │<── trigger_wallet_tx ────│ (e.g., "approve USDC")
│<── wallet:tx_requested ─────│ │
│ │ │
│ [Sign & submit tx] │ │
│ │ │
│──── tx_result ─────────────>│──── tx_result ──────────>│
│ (confirmed + txHash) │ │
│ │ │
│ │<── trigger_wallet_tx ────│ (e.g., "execute swap")
│<── wallet:tx_requested ─────│ │
│ │ │
│ [Sign & submit tx] │ │
│ │ │
│──── tx_result ─────────────>│──── tx_result ──────────>│
│ │ │
│<───── task_response ────────│<────── response ─────────│
└ └ └
sdk.on("wallet:tx_requested", async (data) => {
console.log(`Transaction requested by ${data.agentName}: ${data.description}`);
console.log(`Chain: ${data.tx.chainId}, To: ${data.tx.to}, Value: ${data.tx.value}`);
try {
// Sign and submit using your preferred library
const txHash = await wallet.sendTransaction({
to: data.tx.to,
value: data.tx.value,
data: data.tx.data,
chainId: data.tx.chainId,
});
// Wait for confirmation
const receipt = await provider.waitForTransaction(txHash);
// Report success — include room and chainId for proper routing and tx hash formatting
await sdk.sendTxResult(data.taskId, "confirmed", txHash, undefined, data.room, data.tx.chainId);
} catch (err) {
// Report failure
await sdk.sendTxResult(data.taskId, "failed", undefined, err.message, data.room, data.tx.chainId);
}
});If the transaction is optional (data.optional === true) or you don't want to sign:
await sdk.sendTxResult(data.taskId, "rejected", undefined, undefined, data.room, data.tx.chainId);| Field | Type | Description |
|---|---|---|
taskId |
string |
Task identifier — pass this back in sendTxResult() |
agentName |
string? |
Name of the agent requesting the transaction |
tx.to |
string |
Target contract/address |
tx.value |
string |
Value in wei |
tx.data |
string? |
Encoded calldata (for contract interactions) |
tx.chainId |
number |
Target chain (e.g., 8453 for Base) |
description |
string? |
Human-readable description of the transaction |
optional |
boolean |
Whether the user can skip this transaction |
room |
string? |
Room ID — must be passed back in sendTxResult() for routing |
await sdk.sendTxResult(taskId, status, txHash?, error?, room?, chainId?)| Parameter | Type | Description |
|---|---|---|
taskId |
string |
The taskId from the wallet:tx_requested event |
status |
"confirmed" | "rejected" | "failed" |
Transaction outcome |
txHash |
string? |
On-chain transaction hash (required for "confirmed") |
error |
string? |
Error message (for "failed" status) |
room |
string? |
Room ID from the wallet:tx_requested event (required for routing) |
chainId |
number? |
Chain ID from data.tx.chainId — formats txHash as hash|network (e.g., 0xabc...|base) |
Important: Always pass
data.roomanddata.tx.chainIdfrom the event back intosendTxResult(). The server usesroomto route the result to the correct agent, andchainIdformats the tx hash to match the expected format. Without these, the agent may not receive your response correctly.
Some operations require multiple sequential transactions (e.g., ERC-20 approve followed by a swap). The agent handles sequencing — your code just needs to keep listening:
- Agent sends
trigger_wallet_tx#1 (approve) → you sign → you callsendTxResult() - Agent receives the result, then sends
trigger_wallet_tx#2 (swap) → you sign → you callsendTxResult() - Agent sends
task_responsewith the final outcome
Your wallet:tx_requested listener stays active for the entire lifecycle. Each transaction arrives as a separate event with the same taskId but different tx data. The task is only resolved when the agent sends the final task_response.
The SDK is fully event-driven. Subscribe to what matters:
sdk.on("connection:open", () => console.log("🔌 WebSocket connected"));
sdk.on("connection:close", (code, reason) => console.log(`❌ Disconnected: ${reason}`));
sdk.on("connection:reconnecting", (attempt) => console.log(`🔄 Reconnecting (attempt ${attempt})`));
sdk.on("auth:challenge", (challenge) =>
console.log("🔐 Challenge received, signing with wallet...")
);
sdk.on("auth:success", (state) => {
console.log(`✅ Authenticated as ${state.walletAddress}`);
console.log(`Whitelisted: ${state.isWhitelisted}`);
});
sdk.on("auth:error", (error) => console.error("❌ Auth failed:", error));sdk.on("agent:selected", (selection) => {
console.log(`🤖 ${selection.agentName} was selected by coordinator`);
console.log(`Reasoning: ${selection.reasoning}`);
});
sdk.on("agent:response", (response) => {
console.log(`💬 ${response.agentName}: ${response.humanized}`);
});
sdk.on("agent:list", (agents) => {
console.log(`📋 Agent list updated: ${agents.length} agents available`);
agents.forEach((agent) => {
console.log(` - ${agent.name}: ${agent.capabilities?.map(c => c.name).join(", ")}`);
});
});sdk.on("room:subscribed", (data) => {
console.log(`✅ Joined room: ${data.roomId}`);
console.log(`All subscribed rooms: ${data.subscriptions.join(", ")}`);
});
sdk.on("room:unsubscribed", (data) => {
console.log(`👋 Left room: ${data.roomId}`);
});sdk.on("wallet:tx_requested", async (data) => {
console.log(`🔗 ${data.agentName} requests tx on chain ${data.tx.chainId}`);
console.log(` ${data.description}`);
// See "Wallet Transaction Flow" section for full handling
});const sdk = new TeneoSDK({
wsUrl: "wss://backend.developer.chatroom.teneo-protocol.ai/ws",
privateKey: "0x...", // Your private key
autoJoinPublicRooms: ["room-id-1"],
reconnect: true,
logLevel: "info"
});import { SDKConfigBuilder, SecurePrivateKey } from "@teneo-protocol/sdk";
// Encrypt private key in memory (AES-256-GCM)
const secureKey = new SecurePrivateKey(process.env.PRIVATE_KEY!);
const config = new SDKConfigBuilder()
// Required
.withWebSocketUrl(process.env.TENEO_WS_URL!)
.withAuthentication(secureKey) // Encrypted key
// Rooms - auto-join these public rooms on connect
.withAutoJoinPublicRooms(["room-id-1", "room-id-2"]) // Public rooms only (private rooms auto-available)
// Reconnection strategy
.withReconnectionStrategy({
type: "exponential",
baseDelay: 3000, // Start at 3 seconds
maxDelay: 120000, // Cap at 2 minutes
maxAttempts: 20,
jitter: true // Prevent thundering herd
})
// Webhook with retry
.withWebhook("https://your-server.com/webhook", {
Authorization: "Bearer token"
})
.withWebhookRetryStrategy({
type: "exponential",
baseDelay: 1000,
maxDelay: 30000,
maxAttempts: 5
})
// Response formatting
.withResponseFormat({
format: "both", // 'raw' | 'humanized' | 'both'
includeMetadata: true
})
// Security
.withSignatureVerification({
enabled: true,
trustedAddresses: ["0xAgent1...", "0xAgent2..."],
requireFor: ["task_response"]
})
// Performance
.withMessageDeduplication(true, 60000, 10000)
.withLogging("debug")
.build();
const sdk = new TeneoSDK(config);Create .env:
# Required
TENEO_WS_URL=wss://backend.developer.chatroom.teneo-protocol.ai/ws
PRIVATE_KEY=0xYourPrivateKey
# Optional
LOG_LEVEL=info
# Payment network (v2.3.0) - Optional, defaults to PEAQ
TENEO_NETWORK=base # By name: peaq, base, avalanche
# Or by chain ID: TENEO_NETWORK=8453
# Or CAIP-2: TENEO_NETWORK=eip155:8453Load them:
import * as dotenv from "dotenv";
dotenv.config();
const sdk = new TeneoSDK({
wsUrl: process.env.TENEO_WS_URL!,
privateKey: process.env.PRIVATE_KEY!,
logLevel: (process.env.LOG_LEVEL as any) || "info"
// TENEO_NETWORK is automatically used by SDK for payment network
});All v3.0.0 changes are backward compatible. Old method names still work via deprecated aliases.
Search and replace across your codebase to use the new, clearer names:
# Config property
autoJoinRooms: → autoJoinPublicRooms:
# Room subscription methods
.subscribeToRoom( → .subscribeToPublicRoom(
.unsubscribeFromRoom( → .unsubscribeFromPublicRoom(
# Cache-only methods (sync)
.getRoomAgents( → .getCachedRoomAgents(
.getAvailableAgents( → .getCachedAvailableAgents(
.getRoomAgentCount( → .getCachedRoomAgentCount(
# Boolean check methods
.isAgentInRoom( → .checkAgentInRoom(
# Network-wide search methods
.findAgentsByCapability( → .findAvailableAgentsByCapability(
.findAgentsByName( → .findAvailableAgentsByName(
.findAgentsByStatus( → .findAvailableAgentsByStatus(Room Subscriptions: subscribeToPublicRoom() clarifies that only public rooms need subscription. Private rooms are automatically available after authentication.
Cache Methods: The getCached* prefix makes it obvious these are synchronous, cache-only operations. Async methods like listRoomAgents() still fetch from server.
Boolean Checks: checkAgentInRoom() returns boolean | undefined, not just boolean. The name clarifies this uncertainty.
Search Scope: findAvailableAgentsBy*() methods search ALL available agents network-wide, not just room-specific agents.
- ✅ Old names work indefinitely
- ✅ TypeScript/JSDoc shows deprecation hints
- ✅ Migrate on your schedule
- ✅ Zero functional changes
See CHANGELOG.md for detailed explanations and examples.
Your Ethereum private key is encrypted in memory with AES-256-GCM:
import { SecurePrivateKey } from "@teneo-protocol/sdk";
// Immediately encrypted on construction
const secureKey = new SecurePrivateKey(process.env.PRIVATE_KEY!);
const sdk = new TeneoSDK({
wsUrl: "...",
privateKey: secureKey // Pass encrypted key
});
// Key lifecycle:
// 1. Encrypted in memory with AES-256-GCM
// 2. Only decrypted temporarily during signing
// 3. Zeroed from memory immediately after use
// 4. Auto-cleanup on disconnectPrevents cascading failures in webhook delivery:
const status = sdk.getWebhookStatus();
console.log("Circuit state:", status.queue.circuitState);
// CLOSED = Normal operation, webhooks being delivered
// OPEN = Too many failures, failing fast (60s timeout)
// HALF_OPEN = Testing recovery (2 successes → CLOSED)
// Circuit opens after 5 consecutive failures
// Automatically retries after 60 seconds
// Closes after 2 successful deliveries
// State transitions:
// CLOSED --[5 failures]--> OPEN --[60s]--> HALF_OPEN --[2 successes]--> CLOSEDConfigurable exponential backoff, linear, or constant delays:
| Strategy | Formula | Example (base=2s, mult=2) |
|---|---|---|
| Exponential | base * mult^attempt |
2s, 4s, 8s, 16s, 32s |
| Linear | base * attempt |
2s, 4s, 6s, 8s, 10s |
| Constant | base |
2s, 2s, 2s, 2s, 2s |
const config = new SDKConfigBuilder()
.withReconnectionStrategy({
type: "exponential",
baseDelay: 3000,
maxDelay: 120000,
maxAttempts: 20,
jitter: true // Add 0-1000ms randomness
})
.build();Prevents duplicate message processing with TTL-based cache:
const config = new SDKConfigBuilder()
.withMessageDeduplication(
true, // Enable
300000, // 5 minute TTL
10000 // Cache up to 10k message IDs
)
.build();
// Duplicate messages are automatically filtered
// Useful for preventing replay attacks
// Auto-cleanup at 90% capacityVerify agent messages are authentic:
const config = new SDKConfigBuilder()
.withSignatureVerification({
enabled: true,
trustedAddresses: ["0xAgent1...", "0xAgent2..."],
requireFor: ["task_response", "agent_selected"],
strictMode: false // Only reject types in requireFor
})
.build();
sdk.on("signature:verified", (type, address) => {
console.log(`✅ Verified ${type} from ${address}`);
});
sdk.on("signature:failed", (type, reason) => {
console.warn(`⚠️ Invalid signature on ${type}: ${reason}`);
});const health = sdk.getHealth();
console.log("Status:", health.status); // 'healthy' | 'degraded' | 'unhealthy'
console.log("Connected:", health.connection.status);
console.log("Authenticated:", health.connection.authenticated);
if (health.webhook) {
console.log("Webhook pending:", health.webhook.pending);
console.log("Circuit state:", health.webhook.circuitState);
}
if (health.rateLimit) {
console.log("Available tokens:", health.rateLimit.availableTokens);
}const state = sdk.getConnectionState();
console.log("Connected:", state.connected);
console.log("Authenticated:", state.authenticated);
console.log("Reconnecting:", state.reconnecting);
console.log("Reconnect attempts:", state.reconnectAttempts);// Rate limiter status (via health check)
const health = sdk.getHealth();
if (health.rateLimit) {
console.log("Available:", health.rateLimit.availableTokens);
console.log("Rate:", health.rateLimit.tokensPerSecond, "/sec");
console.log("Burst capacity:", health.rateLimit.maxBurst);
}
// Deduplication cache
const dedup = sdk.getDeduplicationStatus();
if (dedup) {
console.log("Cache size:", dedup.cacheSize);
console.log("Max size:", dedup.maxSize);
console.log("Usage:", Math.round((dedup.cacheSize / dedup.maxSize) * 100), "%");
}Problem:
Error [ERR_REQUIRE_ESM]: require() of ES Module node-fetch not supported
Solution: Use the compiled version:
# ✅ Correct
npm run build
node your-app.js
# ❌ Wrong
npx ts-node your-app.tsAlternative: Install node-fetch v2:
npm install node-fetch@2.7.0
npm run buildProblem: Can't authenticate with Teneo network.
Solutions:
-
Check private key format (64 hex characters, + 0x prefix):
// ✅ Good - 64 hex characters after 0x prefix privateKey: "0x1234567890123456789012345678901234567890123456789012345678901234";
-
Verify key length:
echo -n "0x1234...your_key" | wc -c # Should output: 66 (0x prefix + 64 hex characters)
-
Enable debug logging:
const sdk = new TeneoSDK({ wsUrl: "...", privateKey: "...", logLevel: "debug" });
Problem:
RateLimitError: Rate limit exceeded
Solutions:
- Slow down requests:
for (const message of messages) { await sdk.sendMessage(message, { room: roomId }); await new Promise((r) => setTimeout(r, 200)); // 200ms delay }
Solutions:
-
Verify HTTPS (except localhost):
// ✅ Good webhookUrl: "https://your-server.com/webhook"; webhookUrl: "http://localhost:3000/webhook"; // ❌ Bad webhookUrl: "http://your-server.com/webhook";
-
Test manually:
curl -X POST https://your-server.com/webhook \ -H "Content-Type: application/json" \ -d '{"test": true}'
-
Check circuit breaker:
const status = sdk.getWebhookStatus(); if (status.queue.circuitState === "OPEN") { console.log("Circuit open, will retry in 60s"); }
npm test # All tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
npm run test:unit # Unit tests only
npm run test:integration # Integration testsTest Results:
- ✅ 671 unit tests passing
- ✅ 100% pass rate (Phase 0-2 complete)
- ✅ Comprehensive coverage
We welcome contributions!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests
- Run
npm test - Commit (
git commit -m 'Add amazing feature') - Push (
git push origin feature/amazing-feature) - Open a Pull Request
AGPL-3.0 License
Built with ❤️ by the Teneo Team