Teneo-Protocol-CLI
Execute Teneo Protocol agent commands and handle x402 USDC payments on Base, Avalanche, PEAQ, and XLayer. Query agents, send commands, manage rooms, and proc...
Install
openclaw skills install teneoprotocolcliLatest Release
Compatibility
{}Capabilities
{}Verification
{}Tags
{
"latest": "1.0.3"
}name: teneo-protocol-cli description: Teneo Protocol CLI — discover and query AI agents, manage rooms, and handle x402 USDC micropayments. Teneo agents require x402 payments signed with a private key (see github.com/AIMadeScripts/teneo-agent-sdk). This skill auto-generates its own wallet on first use (AES-256-GCM encrypted) — the owner just sends a small amount of USDC to fund it. Funds can be withdrawn back to the original funder at any time. No user keys are ever requested. Connects to the Teneo Protocol backend (wss://backend.developer.chatroom.teneo-protocol.ai). SDK: https://www.npmjs.com/package/@teneo-protocol/sdk
SECURITY & PRIVACY DECLARATION
Why this skill needs a private key
Teneo Protocol agents use the x402 payment standard — every paid agent query requires a cryptographically signed USDC transaction. Without a private key, there is no way to sign these transactions and no way to pay agents. This is how the Teneo SDK works by design (see: Teneo Agent SDK on GitHub).
How this skill handles it safely
This skill never asks for or touches any existing user wallet.
Instead, on first use it auto-generates a brand new wallet and stores the private key encrypted at rest (AES-256-GCM) in ~/.teneo-wallet/. The owner simply sends a small amount of USDC to fund this auto-generated wallet — that's it.
- The auto-generated key is encrypted with a separate master secret (both files have
0600permissions) - Only cryptographic signatures are transmitted to the network — never the key itself
- Withdrawals can only go back to the first address that funded the wallet (auto-detected from on-chain data, permanently locked)
- The owner can withdraw all funds back to their wallet at any time via
wallet-withdraw - This is standard Web3 practice, identical to how MetaMask or any hardware wallet works
Network connections
- Connects to the Teneo Protocol backend at
wss://backend.developer.chatroom.teneo-protocol.ai/wsvia WebSocket for agent communication and x402 payments - This is the official Teneo Protocol endpoint (see: https://teneo.pro)
- The SDK is published on npm: https://www.npmjs.com/package/@teneo-protocol/sdk
Dependencies (all from npm public registry)
@teneo-protocol/sdk— official Teneo Protocol clientcommander— CLI framework (42M+ weekly downloads)dotenv— environment variable loader (45M+ weekly downloads)viem— Ethereum library for wallet operations (1M+ weekly downloads)
Purpose
Discover and query Teneo Protocol AI agents, manage rooms, and handle x402 USDC micropayments.
Teneo agents charge per query via the x402 payment standard — a private key is required to sign these USDC transactions (this is a protocol requirement, not a skill choice). This skill creates its own wallet automatically so no existing keys are ever needed.
How it works:
- On first use, the skill auto-generates a new wallet (no setup needed)
- The owner sends a small amount of USDC to the generated wallet address
- The skill uses this wallet to sign x402 payments and authenticate with Teneo
- At any time, the owner can withdraw all remaining funds back to their own wallet
The auto-generated wallet IS the Teneo identity. It handles both authentication (signing the WebSocket handshake) and payments (signing x402 USDC transactions). Without funds, paid commands will fail — but free commands and agent discovery always work.
IMPORTANT: Always Show Status Updates
Teneo commands can take 10-30+ seconds. Never leave the user staring at a blank screen. Before and during every step, send a short status message so the user knows what's happening. This is critical for user experience.
Example flow when a user asks "search @elonmusk on X":
🔍 Checking which agents are in the room... ✅ X Platform Agent is in the room. 💰 Requesting price quote for the search... 💳 Quote received: 0.05 USDC. Confirming payment... ⏳ Payment confirmed. Waiting for agent response... ✅ Here are the results:
Rules:
- Before every CLI command, tell the user what you're about to do in plain language
- After each step completes, confirm it before moving to the next step
- If something takes more than a few seconds, send a "still waiting..." or "processing..." update
- On errors, explain what went wrong and what you'll try next — don't just silently retry
Status messages for common operations:
- Discovering agents → "🔍 Fetching the list of available agents on Teneo..."
- Checking room → "📋 Checking which agents are in your room..."
- Adding agent → "➕ Adding [agent] to your room..."
- Getting quote → "💰 Requesting a price quote..."
- Confirming payment → "💳 Confirming payment of X USDC..."
- Waiting for response → "⏳ Waiting for the agent to respond..."
- Searching handles → "🔍 Searching for that handle on [platform]..."
- Wallet check → "👛 Checking your USDC balance..."
Never run multiple commands in silence. Each step should have a visible status update. The user should always know: what's happening right now, and what comes next.
IMPORTANT: Agent Discovery & Room Limits
Finding Agents
Teneo has many agents available across the entire network. Use these commands to discover them:
list-agents→ shows ALL agents on the entire Teneo network with their IDs, commands, capabilities, and pricing. Always start here.agent-details <agentId>→ full details for one agent (commands with exact syntax + pricing)room-agents <roomId>→ shows agents currently IN your room
IMPORTANT: Agent IDs vs Display Names. Agents have an internal ID (e.g. x-agent-enterprise-v2) and a display name (e.g. "X Platform Agent"). You must always use the internal ID for commands — display names with spaces will fail validation.
Each agent's output includes its commands array with: trigger (the command name), argument (exact query format), description, and pricing (cost in USDC). The correct command format is: @{agentId} {trigger} {argument}
⚠️ Agent "Online" ≠ Reachable
An agent can show "status": "online" in agent-details but still be disconnected in your room. The coordinator will report "agent not found or disconnected" when you try to query it. This means:
- Always test an agent with a cheap command first before relying on it
- If an agent is disconnected, look for alternative agents that serve the same purpose (e.g. if
messariis dead,coinmarketcap-agentcan also provide crypto quotes) - Multiple agents often serve overlapping purposes — know your fallbacks
Pre-Query Checklist
Before every agent query, follow this checklist:
- Get agent commands — run
agent-details <agentId>to see exact command syntax and pricing. Never guess commands. - Check agent status — if offline or disconnected, do NOT add to room or query. Find an alternative.
- Check room capacity — run
room-agents <roomId>to see current agents (max 5). If full, remove one or create a new room. - Know your fallbacks — if your target agent is unreachable, check for similar agents already in the room.
- For social media handles — web search first to find the correct
@handlebefore querying. Wrong handles waste money.
Room Rules
Teneo organizes agents into rooms. You MUST understand these rules:
- Maximum 5 agents per room. A room can hold at most 5 agents at a time.
- You can only query agents that are in your room. If an agent is not in the room, commands to it will fail.
- To use a different agent, find it with
list-agents, then add it withadd-agent <roomId> <agentId>. - If the room already has 5 agents, you must first remove one with
remove-agent <roomId> <agentId>before adding another. - Check who is in the room with
room-agents <roomId>before sending commands.
If the room is full or things get confusing, you can always create a fresh room with create-room "Task Name" and invite only the agent(s) needed for the current task. This keeps things clean and avoids swapping.
Always communicate this to the user. When a user asks to use an agent that is not in the room, explain:
- Which agents are currently in the room (and that the limit is 5)
- That the requested agent needs to be added first
- If the room is full, offer two options: remove an agent to make space, or create a new room for the task
- Ask the user to confirm before making changes
Example message to user:
"Your room has 5 agents: [list]. To use [requested agent], I can either remove one or create a new room. What do you prefer?"
Known Agent Commands Reference
X Platform Agent (x-agent-enterprise-v2) — ~$0.001/query on Base:
| Command | Format | Example |
|---|---|---|
user | user @handle | user @okx |
timeline | timeline @handle <count> | timeline @teneo_protocol 5 |
search | search <query> <count> | search teneo protocol 10 |
mention | mention @handle <count> | mention @teneo_protocol 5 |
followers | followers @handle <count> | followers @okx 10 |
followings | followings @handle <count> | followings @okx 10 |
post_content | post_content <ID_or_URL> | post_content 1234567890 |
post_stats | post_stats <ID_or_URL> | post_stats 1234567890 |
deep_post_analysis | deep_post_analysis | |
deep_search | deep_search |
CoinMarketCap Agent (coinmarketcap-agent) — ~$0.001/query on Base:
| Command | Format | Example |
|---|---|---|
quote | quote <symbol> | quote BTC |
Note: Always verify commands with
agent-details <agentId>— agents may update their commands. The above is a reference, not a guarantee.
One-Time Setup
Before first use, run these commands to set up the Teneo CLI tool:
mkdir -p ~/teneo-skill && cd ~/teneo-skill && npm init -y && NODE_OPTIONS="--max-old-space-size=512" npm install --prefer-offline @teneo-protocol/sdk@3.1.1 commander@12.1.0 dotenv@16.4.5 viem@2.21.5 pino-pretty
Then create the CLI script by writing the following content to ~/teneo-skill/teneo.js:
#!/usr/bin/env node
/**
* Teneo Protocol CLI
* SECURITY: Auto-generates a new wallet on first use. Never asks for existing keys.
* The generated key is encrypted at rest (AES-256-GCM) and used for local signing only.
* Only cryptographic signatures are transmitted — never the key itself.
*/
require("dotenv").config();
const { TeneoSDK, SDKConfigBuilder } = require("@teneo-protocol/sdk");
const { Command } = require("commander");
const { createWalletClient, createPublicClient, http, defineChain } = require("viem");
const { privateKeyToAccount, generatePrivateKey } = require("viem/accounts");
const allChains = require("viem/chains");
const nodeCrypto = require("node:crypto");
const nodeFs = require("node:fs");
const nodePath = require("node:path");
const nodeOs = require("node:os");
const WS_URL = process.env.TENEO_WS_URL || "wss://backend.developer.chatroom.teneo-protocol.ai/ws";
// Optional: advanced users can set TENEO_PRIVATE_KEY to use an existing dedicated bot wallet.
// If not set, a new wallet is auto-generated on first use (see requireKey()).
const PRIVATE_KEY = process.env.TENEO_PRIVATE_KEY;
const DEFAULT_ROOM = process.env.TENEO_DEFAULT_ROOM || "";
const DEFAULT_CHAIN = process.env.TENEO_DEFAULT_CHAIN || "base";
// Build chain ID lookup from all 700+ viem-supported chains (Ethereum, Arbitrum,
// Optimism, Polygon, BSC, Base, Avalanche, etc.) — covers all SquidRouter chains.
const CHAIN_BY_ID = {};
for (const key of Object.keys(allChains)) {
const c = allChains[key];
if (c && typeof c === "object" && c.id) CHAIN_BY_ID[c.id] = c;
}
function getChain(chainId) {
if (CHAIN_BY_ID[chainId]) return CHAIN_BY_ID[chainId];
// Fallback: build a minimal chain definition for unknown chain IDs
return defineChain({
id: chainId,
name: `Chain ${chainId}`,
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: [`https://rpc.chain${chainId}.org`] } },
});
}
// ─── Wallet Storage ────────────────────────────────────────────────────────
const WALLET_DIR = nodePath.join(nodeOs.homedir(), ".teneo-wallet");
const WALLET_FILE = nodePath.join(WALLET_DIR, "wallet.json");
const SECRET_FILE = nodePath.join(WALLET_DIR, ".secret");
function ensureWalletDir() {
if (!nodeFs.existsSync(WALLET_DIR)) {
nodeFs.mkdirSync(WALLET_DIR, { recursive: true, mode: 0o700 });
}
}
function getOrCreateMasterSecret() {
ensureWalletDir();
if (nodeFs.existsSync(SECRET_FILE)) {
const hex = nodeFs.readFileSync(SECRET_FILE, "utf8").trim();
return Buffer.from(hex, "hex");
}
const secret = nodeCrypto.randomBytes(32);
nodeFs.writeFileSync(SECRET_FILE, secret.toString("hex"), { mode: 0o600 });
nodeFs.chmodSync(SECRET_FILE, 0o600);
return secret;
}
function encryptPK(pk, masterSecret) {
const iv = nodeCrypto.randomBytes(12);
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", masterSecret, iv);
const encrypted = Buffer.concat([cipher.update(pk, "utf8"), cipher.final()]);
return {
encryptedKey: encrypted.toString("base64"),
iv: iv.toString("base64"),
authTag: cipher.getAuthTag().toString("base64"),
};
}
function decryptPK(encryptedKey, iv, authTag, masterSecret) {
const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", masterSecret, Buffer.from(iv, "base64"));
decipher.setAuthTag(Buffer.from(authTag, "base64"));
const decrypted = Buffer.concat([decipher.update(Buffer.from(encryptedKey, "base64")), decipher.final()]);
return decrypted.toString("utf8");
}
function loadWallet() {
if (!nodeFs.existsSync(WALLET_FILE)) return null;
try { return JSON.parse(nodeFs.readFileSync(WALLET_FILE, "utf8")); } catch { return null; }
}
function saveWallet(data) {
ensureWalletDir();
nodeFs.writeFileSync(WALLET_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
nodeFs.chmodSync(WALLET_FILE, 0o600);
}
function getWalletAddress() {
const wallet = loadWallet();
if (wallet) return wallet.address;
if (PRIVATE_KEY) {
const key = PRIVATE_KEY.startsWith("0x") ? PRIVATE_KEY : `0x${PRIVATE_KEY}`;
return privateKeyToAccount(key).address;
}
fail("No wallet found. Run any command to auto-generate one.");
}
// ─── USDC Chain Config ─────────────────────────────────────────────────────
const USDC_ADDRESSES = {
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
avax: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
peaq: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
xlayer: "0x74b7F16337b8972027F6196A17a631aC6dE26d22",
};
const WALLET_CHAIN_MAP = {
base: allChains.base,
avax: allChains.avalanche,
peaq: defineChain({ id: 3338, name: "PEAQ", nativeCurrency: { name: "PEAQ", symbol: "PEAQ", decimals: 18 }, rpcUrls: { default: { http: ["https://peaq.api.onfinality.io/public"] } } }),
xlayer: defineChain({ id: 196, name: "XLayer", nativeCurrency: { name: "OKB", symbol: "OKB", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.xlayer.tech"] } } }),
};
const ERC20_BALANCE_ABI = [{ inputs: [{ name: "account", type: "address" }], name: "balanceOf", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" }];
const ERC20_TRANSFER_ABI = [{ inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], name: "transfer", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function" }];
const ERC20_TRANSFER_EVENT = { type: "event", name: "Transfer", inputs: [{ name: "from", type: "address", indexed: true }, { name: "to", type: "address", indexed: true }, { name: "value", type: "uint256", indexed: false }] };
async function detectFunder(walletAddress) {
for (const chainName of ["base", "avax", "peaq", "xlayer"]) {
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) continue;
try {
const client = createPublicClient({ chain, transport: http() });
const logs = await client.getLogs({ address: usdcAddr, event: ERC20_TRANSFER_EVENT, args: { to: walletAddress }, fromBlock: 0n, toBlock: "latest" });
if (logs.length > 0) {
logs.sort((a, b) => Number((a.blockNumber ?? 0n) - (b.blockNumber ?? 0n)));
const from = logs[0].args.from;
if (from) return { funder: from, chain: chainName };
}
} catch {}
}
return null;
}
// Register wallet transaction signer on SDK instance.
// When agents (e.g. SquidRouter) request an on-chain transaction,
// this handler signs and broadcasts it using the bot's own local private key.
// The key NEVER leaves the machine — only the signed transaction is broadcast.
// Supports ALL chains — viem has 700+ built-in chain definitions.
function registerTxSigner(sdk) {
const key = requireKey();
const account = privateKeyToAccount(key.startsWith("0x") ? key : `0x${key}`);
sdk.on("wallet:tx_requested", async (data) => {
const { taskId, tx, agentName, description } = data;
console.error(JSON.stringify({
info: `Transaction requested by ${agentName || "agent"}`,
description: description || "on-chain transaction",
to: tx.to, value: tx.value, chainId: tx.chainId
}));
try {
const chain = getChain(tx.chainId);
const walletClient = createWalletClient({
account,
chain,
transport: http(),
});
const txHash = await walletClient.sendTransaction({
to: tx.to,
value: tx.value ? BigInt(tx.value) : 0n,
data: tx.data || undefined,
chain,
});
console.error(JSON.stringify({ info: `Transaction sent`, txHash, chainId: tx.chainId }));
await sdk.sendTxResult(taskId, "confirmed", txHash);
} catch (err) {
console.error(JSON.stringify({ error: `Transaction failed: ${err.message}` }));
await sdk.sendTxResult(taskId, "failed", undefined, err.message);
}
});
}
function out(data) { console.log(JSON.stringify(data, null, 2)); }
function fail(msg) { console.error(JSON.stringify({ error: msg })); process.exit(1); }
function requireKey() {
// Tier 1: Environment variable
if (PRIVATE_KEY) return PRIVATE_KEY;
// Tier 2: Encrypted wallet file
const wallet = loadWallet();
if (wallet) {
const secret = getOrCreateMasterSecret();
return decryptPK(wallet.encryptedKey, wallet.iv, wallet.authTag, secret);
}
// Tier 3: Auto-generate new wallet
const masterSecret = getOrCreateMasterSecret();
const newKey = generatePrivateKey();
const account = privateKeyToAccount(newKey);
const encrypted = encryptPK(newKey, masterSecret);
saveWallet({
version: 1,
address: account.address,
encryptedKey: encrypted.encryptedKey,
iv: encrypted.iv,
authTag: encrypted.authTag,
createdAt: new Date().toISOString(),
funder: null,
});
console.error(JSON.stringify({
info: "Wallet auto-generated",
address: account.address,
note: "Send USDC to this address on base, avax, peaq, or xlayer to start using paid agents.",
}));
return newKey;
}
function resolveRoom(opt) {
const room = opt || DEFAULT_ROOM;
if (!room) fail("Room ID required. Pass --room <id> or set TENEO_DEFAULT_ROOM.");
return room;
}
const MAX_RETRIES = 3;
const RETRY_DELAY = 5000;
const SHORT_TIMEOUT = 20000; // Fast-fail first attempt (20s) — agent responds in <10s
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function buildSDK(key, opts) {
const builder = new SDKConfigBuilder()
.withWebSocketUrl(WS_URL)
.withAuthentication(key)
.withReconnection({ enabled: true, delay: 3000, maxAttempts: 5 })
.withAutoSummon(true)
.withCache(true, 600000, 500);
if (opts?.autoJoinRoom && !opts.autoJoinRoom.startsWith("private_")) builder.withAutoJoinPublicRooms([opts.autoJoinRoom]);
if (opts?.payments) builder.withPayments({ autoApprove: true, quoteTimeout: 120000 });
return new TeneoSDK(builder.build());
}
// On timeout: remove agent from room + re-add to force fresh WS handshake on Teneo side
async function kickAgent(sdk, roomId, agentId) {
try {
console.error(JSON.stringify({ warn: `Kicking agent ${agentId} from room to reset dangling WebSocket...` }));
await sdk.removeAgentFromRoom(roomId, agentId);
await sleep(2000);
await sdk.addAgentToRoom(roomId, agentId);
await sleep(3000); // Let agent re-register
console.error(JSON.stringify({ info: `Agent ${agentId} re-added to room ${roomId}.` }));
} catch (e) {
console.error(JSON.stringify({ warn: `Kick failed (non-fatal): ${e.message}` }));
}
}
async function withSDK(fn, opts) {
let lastErr;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
let sdk = null;
try {
const key = requireKey();
sdk = buildSDK(key, opts);
await sdk.connect();
registerTxSigner(sdk);
return await fn(sdk, attempt);
} catch (err) {
lastErr = err;
const isTimeout = err.message && (err.message.includes("timeout") || err.message.includes("Timeout"));
// On timeout with a known agent+room, try kicking the agent to reset its WS
if (isTimeout && opts?.kickAgent && opts?.autoJoinRoom && sdk) {
try { await kickAgent(sdk, opts.autoJoinRoom, opts.kickAgent); } catch {}
}
if (sdk) try { sdk.disconnect(); } catch {}
sdk = null;
if (attempt < MAX_RETRIES) {
console.error(JSON.stringify({ warn: `Attempt ${attempt}/${MAX_RETRIES} failed: ${err.message}. Retrying in ${RETRY_DELAY/1000}s...` }));
await sleep(RETRY_DELAY);
}
} finally {
if (sdk) try { sdk.disconnect(); } catch {}
}
}
fail(`All ${MAX_RETRIES} attempts failed. Last error: ${lastErr?.message || lastErr}`);
}
const program = new Command();
program.name("teneo").version("1.0.0")
.description("Teneo Protocol CLI. Private keys are NEVER transmitted.");
program.command("health").description("Check connection health").action(async () => {
await withSDK(async (sdk) => {
const h = sdk.getHealth();
out({ status: h.status, connection: h.connection, agents: h.agents, rooms: h.rooms });
});
});
// NOTE: sdk.getAgents() returns empty — it doesn't work for regular users.
// Workaround: monkey-patch handleAgentDetails and send raw get_agent_details message.
program.command("list-agents").description("List all agents on the Teneo network")
.action(async () => {
await withSDK(async (sdk) => {
let captured = null;
const orig = sdk.agents.handleAgentDetails.bind(sdk.agents);
sdk.agents.handleAgentDetails = (data) => { captured = data; orig(data); };
sdk.ws.send(JSON.stringify({ type: "get_agent_details" }));
for (let i = 0; i < 30 && !captured; i++) await sleep(200);
if (!captured) fail("Timeout waiting for agent list from backend.");
const agents = Array.isArray(captured) ? captured : (captured.agents || [captured]);
out({ count: agents.length, agents: agents.map(a => ({
id: a.id || a.agent_id, name: a.name || a.agent_name, description: a.description,
status: a.status, type: a.type, capabilities: a.capabilities, commands: a.commands
}))});
});
});
// NOTE: sdk.getAgentDetails() hangs forever — promise never resolves.
// Workaround: monkey-patch handleAgentDetails to intercept the response.
program.command("agent-details").description("Get agent details").argument("<agentId>")
.action(async (agentId) => {
await withSDK(async (sdk) => {
let captured = null;
const orig = sdk.agents.handleAgentDetails.bind(sdk.agents);
sdk.agents.handleAgentDetails = (data) => { captured = data; orig(data); };
sdk.ws.send(JSON.stringify({ type: "get_agent_details", agent_id: agentId }));
for (let i = 0; i < 30 && !captured; i++) await sleep(200);
if (!captured) fail("Timeout waiting for agent details from backend.");
const agents = Array.isArray(captured) ? captured : (captured.agents || [captured]);
const match = agents.find(a => (a.id || a.agent_id) === agentId) || agents[0];
out(match || { error: `Agent ${agentId} not found` });
});
});
// NOTE: sendMessage / "send" is DISABLED — backend returns 503 "AI coordinator is disabled".
// All agent interaction must go through direct commands with the quote→confirm payment flow.
// IMPORTANT: <agent> must be the internal agent ID (e.g. "x-agent-enterprise-v2"), NOT the display name.
// The command format sent to the agent is: @{agentId} {trigger} {argument}
program.command("command").description("Direct command to agent (use internal agent ID, not display name)")
.argument("<agent>", "Internal agent ID (e.g. x-agent-enterprise-v2)")
.argument("<cmd>", "Command string: {trigger} {argument}")
.option("--room <roomId>").option("--timeout <ms>", "", "120000").option("--chain <chain>")
.action(async (agent, cmd, opts) => {
const room = resolveRoom(opts.room);
await withSDK(async (sdk, attempt) => {
const timeout = attempt === 1 ? SHORT_TIMEOUT : parseInt(opts.timeout);
const r = await sdk.sendDirectCommand({ agent, command: cmd, room, ...(opts.chain ? { network: opts.chain } : {}) }, true);
// Agent response often arrives 1-3s after confirmQuote resolves — wait to capture it
if (!r || (!r.humanized && !r.raw)) {
await sleep(4000);
out({ status: "sent", note: "Command sent with payment. Response may arrive asynchronously." });
} else {
out({ humanized: r.humanized, raw: r.raw, metadata: r.metadata });
}
}, { autoJoinRoom: room, payments: true, kickAgent: agent });
});
program.command("quote").description("Request price quote (no execution)")
.argument("<message>").option("--room <roomId>").option("--chain <chain>")
.action(async (message, opts) => {
const room = resolveRoom(opts.room);
await withSDK(async (sdk) => {
const q = await sdk.requestQuote(message, room, opts.chain || DEFAULT_CHAIN);
out({ taskId: q.taskId, agentId: q.agentId, agentName: q.agentName, command: q.command, pricing: q.pricing, expiresAt: q.expiresAt, network: opts.chain || DEFAULT_CHAIN });
}, { autoJoinRoom: room, payments: true });
});
// NOTE: confirmQuote resolves BEFORE the agent response arrives (~1-3s delay).
// The actual data comes as a separate WebSocket message. We wait to capture it.
program.command("confirm").description("Confirm quoted task with payment")
.argument("<taskId>").option("--room <roomId>").option("--timeout <ms>", "", "120000")
.action(async (taskId, opts) => {
const room = resolveRoom(opts.room);
await withSDK(async (sdk) => {
const r = await sdk.confirmQuote(taskId, { waitForResponse: true, timeout: parseInt(opts.timeout) });
if (r && (r.humanized || r.raw)) {
out({ humanized: r.humanized, raw: r.raw, metadata: r.metadata });
} else {
// Wait for the actual agent response to arrive via WebSocket
await sleep(4000);
out({ status: "confirmed", note: "Payment sent. Agent response may arrive asynchronously — check room messages." });
}
}, { autoJoinRoom: room, payments: true });
});
program.command("rooms").description("List all rooms").action(async () => {
await withSDK(async (sdk) => {
const rooms = await sdk.listRooms();
out({ count: rooms.length, rooms: rooms.map(r => ({ id: r.id, name: r.name, is_public: r.is_public, is_owner: r.is_owner, description: r.description })) });
});
});
program.command("room-agents").description("List agents in room").argument("<roomId>")
.action(async (roomId) => {
await withSDK(async (sdk) => {
const agents = await sdk.listRoomAgents(roomId);
out({ roomId, count: agents.length, agents: agents.map(a => ({ id: a.agent_id, name: a.agent_name, status: a.status })) });
});
});
program.command("create-room").description("Create room").argument("<name>")
.option("--description <desc>").option("--public", "", false)
.action(async (name, opts) => {
await withSDK(async (sdk) => {
const r = await sdk.createRoom({ name, description: opts.description, isPublic: opts.public });
out({ status: "created", room: { id: r.id, name: r.name, is_public: r.is_public } });
});
});
program.command("update-room").description("Update room").argument("<roomId>")
.option("--name <name>").option("--description <desc>")
.action(async (roomId, opts) => {
await withSDK(async (sdk) => {
const updates = {};
if (opts.name) updates.name = opts.name;
if (opts.description) updates.description = opts.description;
out({ status: "updated", room: await sdk.updateRoom(roomId, updates) });
});
});
program.command("delete-room").description("Delete room").argument("<roomId>")
.action(async (roomId) => {
await withSDK(async (sdk) => { await sdk.deleteRoom(roomId); out({ status: "deleted", roomId }); });
});
program.command("add-agent").description("Add agent to room").argument("<roomId>").argument("<agentId>")
.action(async (roomId, agentId) => {
await withSDK(async (sdk) => { await sdk.addAgentToRoom(roomId, agentId); out({ status: "added", roomId, agentId }); });
});
program.command("remove-agent").description("Remove agent from room").argument("<roomId>").argument("<agentId>")
.action(async (roomId, agentId) => {
await withSDK(async (sdk) => { await sdk.removeAgentFromRoom(roomId, agentId); out({ status: "removed", roomId, agentId }); });
});
program.command("owned-rooms").description("List rooms you own").action(async () => {
await withSDK(async (sdk) => {
const rooms = sdk.getOwnedRooms();
out({ count: rooms.length, rooms: rooms.map(r => ({ id: r.id, name: r.name, is_public: r.is_public })) });
});
});
program.command("shared-rooms").description("List rooms shared with you").action(async () => {
await withSDK(async (sdk) => {
const rooms = sdk.getSharedRooms();
out({ count: rooms.length, rooms: rooms.map(r => ({ id: r.id, name: r.name, is_public: r.is_public })) });
});
});
program.command("subscribe").description("Subscribe to public room").argument("<roomId>")
.action(async (roomId) => {
await withSDK(async (sdk) => { await sdk.subscribeToPublicRoom(roomId); out({ status: "subscribed", roomId }); });
});
program.command("unsubscribe").description("Unsubscribe from room").argument("<roomId>")
.action(async (roomId) => {
await withSDK(async (sdk) => { await sdk.unsubscribeFromPublicRoom(roomId); out({ status: "unsubscribed", roomId }); });
});
// ─── Wallet Management ─────────────────────────────────────────────────────
program.command("wallet-init").description("Generate a new wallet (auto-called on first use)")
.action(async () => {
const existing = loadWallet();
if (existing) { out({ status: "exists", address: existing.address, createdAt: existing.createdAt }); return; }
if (PRIVATE_KEY) { out({ status: "env_var_set", note: "Private key found in environment. No wallet file needed." }); return; }
requireKey();
const wallet = loadWallet();
out({ status: "created", address: wallet.address, createdAt: wallet.createdAt, note: "Send USDC to this address on base, avax, peaq, or xlayer to start using paid agents." });
});
program.command("wallet-address").description("Show wallet public address")
.action(async () => {
const wallet = loadWallet();
if (wallet) { out({ address: wallet.address, createdAt: wallet.createdAt }); }
else if (PRIVATE_KEY) {
const key = PRIVATE_KEY.startsWith("0x") ? PRIVATE_KEY : `0x${PRIVATE_KEY}`;
out({ address: privateKeyToAccount(key).address, source: "environment_variable" });
} else {
requireKey();
const w = loadWallet();
out({ address: w.address, createdAt: w.createdAt });
}
});
program.command("wallet-export-key").description("Export private key (DANGEROUS)")
.action(async () => {
const wallet = loadWallet();
if (!wallet) { fail(PRIVATE_KEY ? "No wallet file found. Key is in an environment variable." : "No wallet found. Run wallet-init first."); return; }
const secret = getOrCreateMasterSecret();
const key = decryptPK(wallet.encryptedKey, wallet.iv, wallet.authTag, secret);
console.error(JSON.stringify({ warning: "PRIVATE KEY EXPORTED. Never share this. Never paste into websites. Never commit to git." }));
out({ address: wallet.address, privateKey: key });
});
program.command("wallet-balance").description("Check USDC balance on supported chains")
.option("--chain <chain>", "Specific chain (base|avax|peaq|xlayer)")
.action(async (opts) => {
const address = getWalletAddress();
const chainsToCheck = opts.chain ? [opts.chain] : ["base", "avax", "peaq", "xlayer"];
const results = {};
for (const chainName of chainsToCheck) {
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) { results[chainName] = { error: `Unknown chain: ${chainName}` }; continue; }
try {
const client = createPublicClient({ chain, transport: http() });
const balance = await client.readContract({ address: usdcAddr, abi: ERC20_BALANCE_ABI, functionName: "balanceOf", args: [address] });
results[chainName] = { usdc: (Number(balance) / 1e6).toFixed(6), raw: balance.toString() };
} catch (err) { results[chainName] = { error: err.message }; }
}
out({ address, balances: results });
});
program.command("wallet-withdraw").description("Withdraw USDC back to original funder ONLY")
.argument("<amount>", "Amount in USDC").argument("<chain>", "Chain (base|avax|peaq|xlayer)")
.action(async (amountStr, chainName) => {
const wallet = loadWallet();
if (!wallet) fail("No wallet file found.");
let destination = wallet.funder;
if (!destination) {
console.error(JSON.stringify({ info: "No funder locked yet. Scanning chains for incoming USDC transfers..." }));
const result = await detectFunder(wallet.address);
if (!result) fail("No incoming USDC transfers found. Cannot determine funder address.");
wallet.funder = result.funder;
saveWallet(wallet);
destination = result.funder;
console.error(JSON.stringify({ info: `Funder auto-detected and locked: ${destination} (${result.chain})` }));
}
const amount = parseFloat(amountStr);
if (isNaN(amount) || amount <= 0) fail("Invalid amount.");
const rawAmount = BigInt(Math.round(amount * 1e6));
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) fail(`Unknown chain: ${chainName}`);
const secret = getOrCreateMasterSecret();
const pk = decryptPK(wallet.encryptedKey, wallet.iv, wallet.authTag, secret);
const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
const wc = createWalletClient({ account, chain, transport: http() });
const txHash = await wc.writeContract({ address: usdcAddr, abi: ERC20_TRANSFER_ABI, functionName: "transfer", args: [destination, rawAmount] });
out({ status: "sent", txHash, amount: amountStr, chain: chainName, destination, note: "Funds returned to original funder address." });
});
program.command("wallet-detect-funder").description("Detect and lock the first address that sent USDC to this wallet")
.action(async () => {
const wallet = loadWallet();
if (!wallet) fail("No wallet file found. Run wallet-init first.");
if (wallet.funder) { out({ funder: wallet.funder, locked: true, note: "Funder already locked. Cannot be changed." }); return; }
console.error(JSON.stringify({ info: "Scanning all chains for incoming USDC transfers..." }));
const result = await detectFunder(wallet.address);
if (!result) { out({ funder: null, note: "No incoming USDC transfers found yet. Send USDC to this wallet first." }); return; }
wallet.funder = result.funder;
saveWallet(wallet);
out({ funder: result.funder, chain: result.chain, locked: true, note: "Funder detected and permanently locked. Withdrawals will only go to this address." });
});
program.parseAsync(process.argv).catch(err => fail(err.message || String(err)));
Make the script executable:
chmod +x ~/teneo-skill/teneo.js
Alternative: Extract teneo.js from this skill file automatically:
sed -n '/```javascript/,/```$/p' skill.md | head -n -1 | tail -n +2 > ~/teneo-skill/teneo.js && chmod +x ~/teneo-skill/teneo.js
Wallet & Authentication
No setup required. On first use, the skill auto-generates a new wallet, encrypts the key (AES-256-GCM), and stores it at ~/.teneo-wallet/wallet.json. The wallet address is printed so you know where to send USDC.
The auto-generated key serves two purposes:
- Authentication — signs the WebSocket handshake to prove identity on Teneo
- Payment — signs x402 USDC transactions to pay agents
If the wallet has no USDC, paid commands will fail. Fund the wallet first.
Advanced (optional): If you already have a dedicated bot wallet, you can set
TENEO_PRIVATE_KEYin the environment. The skill will use it instead of auto-generating. Most users should just use the auto-generated wallet.
Wallet Management Commands
node ~/teneo-skill/teneo.js wallet-init # Generate wallet (auto-called on first use)
node ~/teneo-skill/teneo.js wallet-address # Show public address
node ~/teneo-skill/teneo.js wallet-balance # Check USDC balance on all chains
node ~/teneo-skill/teneo.js wallet-balance --chain base # Check balance on specific chain
node ~/teneo-skill/teneo.js wallet-detect-funder # Detect & lock the first address that sent USDC
node ~/teneo-skill/teneo.js wallet-withdraw 5.00 base # Withdraw USDC to funder address ONLY
node ~/teneo-skill/teneo.js wallet-export-key # Export private key (handle with care!)
Safety: Auto-Detected Funder Lock
- The first address that sends USDC to this wallet is automatically detected and permanently locked as the funder
wallet-withdrawcan ONLY send funds back to this locked funder address- There is no
--toflag and no way to manually set the funder — it is always auto-detected from on-chain data - Once locked, the funder address cannot be changed
- Detection happens automatically on first withdrawal, or manually via
wallet-detect-funder
Wallet Security
- Private key encrypted at rest with AES-256-GCM
- Master secret and wallet data in separate files (leaking one is useless without the other)
- Both files have
0600permissions (owner-only read/write) - Key NEVER logged, transmitted, or included in any API call
Optional environment variables
TENEO_WS_URL— WebSocket endpoint (default:wss://backend.developer.chatroom.teneo-protocol.ai/ws)TENEO_DEFAULT_ROOM— Default room ID (so you don't need--roomevery time)TENEO_DEFAULT_CHAIN— Default payment chain:base,avax,peaq, orxlayer(default:base)
How to Use
All commands are run as:
node ~/teneo-skill/teneo.js <command> [options]
Health Check
node ~/teneo-skill/teneo.js health
List ALL Agents on Teneo Network
node ~/teneo-skill/teneo.js list-agents # ALL agents with IDs, commands + pricing
Agent Details
node ~/teneo-skill/teneo.js agent-details <agentId> # e.g. agent-details x-agent-enterprise-v2
Direct Agent Command
# Use the INTERNAL agent ID (not display name). Format: command <agentId> "<trigger> <argument>"
node ~/teneo-skill/teneo.js command "x-agent-enterprise-v2" "user @okx" --room <roomId>
node ~/teneo-skill/teneo.js command "weather-agent-v1" "forecast New York" --room <roomId> --chain base
Note: The
sendcommand (auto-routing) is disabled on the backend (returns 503). Always usecommandwith a specific agent ID.
Request Quote (No Execution)
node ~/teneo-skill/teneo.js quote "Analyze market trends" --room <roomId>
Confirm & Pay
node ~/teneo-skill/teneo.js confirm <taskId> --room <roomId>
Room Management
node ~/teneo-skill/teneo.js rooms
node ~/teneo-skill/teneo.js owned-rooms
node ~/teneo-skill/teneo.js shared-rooms
node ~/teneo-skill/teneo.js create-room "Research Lab" --description "Crypto research" --public
node ~/teneo-skill/teneo.js update-room <roomId> --name "New Name"
node ~/teneo-skill/teneo.js delete-room <roomId>
node ~/teneo-skill/teneo.js room-agents <roomId>
node ~/teneo-skill/teneo.js add-agent <roomId> <agentId>
node ~/teneo-skill/teneo.js remove-agent <roomId> <agentId>
node ~/teneo-skill/teneo.js subscribe <roomId>
node ~/teneo-skill/teneo.js unsubscribe <roomId>
Output Format
All commands return JSON to stdout. Errors return {"error": "message"} to stderr.
Typical Workflow
- Ensure wallet is funded — run
wallet-balanceto check USDC. If empty, tell the user your wallet address (wallet-address) and ask them to send USDC. Paid commands will not work without funds. - Check your room — run
room-agents <roomId>to see which agents are in your room (max 5) - Discover ALL agents — run
list-agentsto see every agent on the Teneo network, their internal IDs, commands, and pricing - Add agents to your room — use
add-agent <roomId> <agentId>to add agents you need (remove one first if room is full) - Verify the agent is reachable — an agent can show "online" but be disconnected. Test with a cheap command first.
- Send a command:
command "<agentId>" "<trigger> <argument>" --room <room>— always use the internal agent ID, not the display name - For manual payment flow: First
quoteto see the price, thenconfirmwith the taskId. Note:commandwithautoApprove: truehandles payment automatically. - Swap agents as needed — always tell the user when you need to remove an agent to make room for another. If an agent is dead, find an alternative.
- Set TENEO_DEFAULT_ROOM after creating a room so you don't need
--roomevery time
Searching for Users / Handles on Platforms
When a user asks to look up a social media account, there are two paths:
With @ handle (direct query)
If the user provides an exact handle with @ (e.g. @teneo_protocol), query the agent directly — this will fetch the profile immediately without searching first.
Without @ (web search first, then query)
If the user provides a name without @ (e.g. "teneo protocol"), you must find the correct handle first. Never guess handles — wrong handles waste money ($0.001 each) and return wrong data.
Step 1: Web search to find the correct handle. Tell the user:
"🔍 Searching the web for the correct handle..."
Use a web search (not the Teneo agent) to find the official handle. Look for:
- The most prominent result (highest followers, verified badge)
- Official website links that confirm the handle
- Be careful of impostor/dead accounts with similar names
Real example: Searching for "teneo protocol twitter" returns:
@TENEOprotocol— 120 followers, dead account ❌@teneo_protocol— 303K followers, active, official ✅
Always pick the most prominent, verified, active account.
Step 2: Check for handle changes. Sometimes an account's bio says "we are now @newhandle on X" (e.g. @peaqnetwork → @peaq). If you see this, use the new handle.
Step 3: Query with the confirmed handle.
"✅ Found the handle: @teneo_protocol. Now querying their profile..."
Always tell the user on first use: Using @handle (e.g. @teneo_protocol) queries directly and is faster. Without the @, I need to search the web first to find the right handle.
Additional tips
- Check the agent's available commands with
agent-details <agentId>to see the correct syntax - If a query fails, try the opposite format: with
@if you tried without, or without if you tried with@ - Some agents only accept handles, others accept search terms — check the command's
argumentfield
Payment Chains
Supported blockchains for USDC payments:
- base — Ethereum L2 (default)
- avax — Avalanche
- peaq — PEAQ network
- xlayer — XLayer (OKX L2)
If funds are insufficient on the default chain, try a different chain with --chain.
Known Issues & Workarounds
These are real-world issues discovered in production. They are already handled in the code/docs above, but documented here so you understand why things work the way they do.
- OOM on small instances.
npm installgets killed on low-memory VMs. Fix: useNODE_OPTIONS="--max-old-space-size=512"and--prefer-offlineduring install. - Missing
pino-prettydependency. The@teneo-protocol/sdkrequirespino-prettyat runtime but doesn't list it as a dependency. Must install explicitly. sdk.getAgents()returns empty. The SDK's built-in method doesn't work for regular users. Workaround: thelist-agentscommand monkey-patcheshandleAgentDetailsand sends a rawget_agent_detailsWebSocket message, which works for everyone.getAgentDetails()hangs forever. The SDK receives the data internally (logs show "Agent details received") but the promise never resolves. Workaround: monkey-patchsdk.agents.handleAgentDetailsto intercept the response.sendDirectCommandsilently fails without payments. WithoutwithPayments({ autoApprove: true }), the SDK uses a legacy flow that sends the message but never gets a response. Always enable payments.- AI coordinator is disabled.
sendMessage()(auto-routing) returns 503. Only direct@agentcommands work. Do NOT use thesendcommand. - Agent IDs with spaces fail. The SDK's
AgentIdSchemaonly allows[a-zA-Z0-9_-]. Always use the internal agent ID (e.g.x-agent-enterprise-v2), never the display name (e.g. "X Platform Agent"). confirmQuoteresolves before agent response. The actual data arrives as a separate WebSocket message ~1-3s after confirmation. The code adds a wait to capture it.available-agents/listAvailableAgents()triggers protocol errors. Returnsagent_owner_wallets unknown message typedue to SDK/backend version mismatch. Useroom-agentsto see what's in a room.agent_owner_walletsunknown message type. The backend sends this message on every room operation, causing Zod validation errors in the SDK. This is non-fatal noise from a protocol version mismatch between SDK 3.1.1 and the current backend. Ignore it.- Agents can be "online" but disconnected. An agent may show
"status": "online"inagent-detailsbut the coordinator reports "agent not found or disconnected" when queried. Always test with a cheap command first. If disconnected, find an alternative agent. - Multiple agents serve overlapping purposes. If your target agent is offline/disconnected, check for alternatives already in the room. Example:
messariwas dead butcoinmarketcap-agentcould provide crypto quotes. - Wrong social handles waste money. Never guess a handle — web search first. Example:
@TENEOprotocol(120 followers, dead) vs@teneo_protocol(303K followers, real). Each wrong query costs ~$0.001.
