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
- 🔐 Web3-native authentication using Ethereum wallet signatures (no API keys!)
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 } from "@teneo-protocol/sdk";
// 1. 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
});
// 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 │
└─────────┴─────────┴─────────┴─────────┘
Unlike traditional APIs with API keys, Teneo uses Ethereum wallet signatures:
// 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 machineThis enables:
- 🔐 No API keys to manage - Your wallet IS your identity
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}`);
});// 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.
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 ─────>│
└ └ └
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}`);
});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