Talkory Node.js SDK
Ask once, hear from up to 6 LLMs and get back an AI consensus - fully typed, zero runtime dependencies. Five run by default (GPT, Claude, Gemini, Perplexity, Grok); Kimi K3 is opt-in. Install with npm install talkory.
npm install talkory
// quickstart.ts
import { Talkory } from "talkory";
const client = new Talkory(); // reads TALKORY_API_KEY
const result = await client.queries.run(
"Best commuter motorcycles under $5000?"
);
console.log(result.consensus);
console.log(result.confidence);Installation
The SDK targets Node.js 18+ (or any runtime with a global fetch, such as Bun or Deno), has zero runtime dependencies (built on the platform's native fetch), and ships as a dual ESM/CJS package with bundled TypeScript declarations.
npm install talkory| Detail | Value |
|---|---|
| Node.js | 18+ (also works on Bun, Deno, or any runtime with global fetch) |
| Dependencies | None at runtime |
| Module formats | ESM (import) and CommonJS (require), both typed |
| License | MIT |
Works with either module system out of the box:
// ESM / TypeScript
import { Talkory } from "talkory";
// CommonJS
const { Talkory } = require("talkory");Quickstart
Every method is async and returns a typed Promise: result.consensus is the synthesized answer, result.commonAnswer is the content every model agreed on, and result.confidence is a 0-100 score.
import { Talkory } from "talkory";
const client = new Talkory(); // reads TALKORY_API_KEY from the environment
const result = await client.queries.run("Best commuter motorcycles under $5000?");
console.log(result.consensus); // synthesized consensus answer
console.log(result.commonAnswer); // what the models agreed on
console.log(result.confidence); // 0-100
console.log(result.charged); // USD charged for this runqueries.run() waits for every model plus the consensus before resolving (can take a few minutes); its default timeout is 130 minutes.
Authentication
Create a key at app.talkory.ai โ Settings โ API Keys, then either set it as an environment variable or pass it explicitly:
export TALKORY_API_KEY="tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"import { Talkory } from "talkory";
// reads TALKORY_API_KEY automatically
const client = new Talkory();
// or pass a key explicitly
const client2 = new Talkory({ apiKey: "tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" });toString(), console.log, and error messages all show at most tk_live_***a1b2. Every request is sent over HTTPS (a plain http:// baseUrl is refused unless you explicitly pass allowInsecure: true, for local testing only) with a User-Agent: talkory-js/{version} {runtime} header.Streaming
client.queries.stream() returns an AsyncGenerator you consume with for await...of, yielding typed events as each model responds live, ending with a done event carrying the consensus.
for await (const event of client.queries.stream("Compare Python web frameworks in 2026")) {
if (event.type === "model_chunk") {
process.stdout.write(event.chunk);
} else if (event.type === "done") {
console.log("\n\nConsensus:", event.consensus);
}
}Events are a discriminated union on type: query_created, model_stream_start, model_chunk, model_result, model_error, round1_done, round2_start, round2_done, consensus_start, and done - TypeScript narrows the event's fields automatically once you check event.type. Any future event type your SDK version doesn't know about arrives as an UnknownEvent instead of throwing, so streams keep working across versions.
StreamInterruptedError carrying a queryId. The run keeps going on our side and is still charged, so recover the result with client.queries.retrieve(err.queryId) instead of re-running the prompt (which would charge you twice).Choosing models & options
run(), create() and stream() all accept the same options object.
| Option | Type | Default | Description |
|---|---|---|---|
models | string[] | default set (5 models) | Model ids from models.list() (max 10). Omit to run the default set: gpt, claude, gemini, perplexity, grok. Opt-in models - returned with default: false, currently Kimi K3 ("kimi") - are excluded from that set and run only when you name them. |
country | string | "global" | ISO country code (e.g. "in") to localise answers. |
recursive | boolean | false | Round-2 self-review: each model critiques and improves its own answer before consensus. |
consensus | boolean | true | false skips consensus generation (cheaper); generate it later with queries.consensus(). |
// default set (5 models)
await client.queries.run("...");
// explicitly include the opt-in model
await client.queries.run("...", { models: ["gpt", "claude", "kimi"] });const result = await client.queries.run("Is a heat pump worth it in Delhi?", {
models: ["gpt", "claude"], // subset (max 10)
recursive: true, // round-2 self-review before consensus
country: "in", // localization
});Deferred consensus, generated later (idempotent - cached re-calls charge $0):
const result = await client.queries.run("...", { consensus: false });
const later = await client.queries.consensus(result.id);models.list()
client.models.list() is a free call that returns the models the server has configured, as an array of ModelInfo objects:
const models = await client.models.list(); // free| Field | Type | Notes |
|---|---|---|
id | string | use this in models on a query |
name | string | display name, e.g. "Claude Sonnet 5" |
provider | string | e.g. "Anthropic" |
vision | boolean | accepts image input |
default | boolean (optional) | true = runs when you omit models. false = opt-in, runs only when named. Older API deployments omit the field; treat undefined as true. |
Only models configured on the server are returned - a model missing from this list cannot be queried.
So the opt-in models are the ones you have to ask for by name:
const optIn = (await client.models.list()).filter((m) => m.default === false);Key features
- Async-first, no callbacks. Every call is a
PromiseorAsyncGenerator-awaita result, orfor await...ofa stream. - Auto-paginating history.
client.queries.list()is an async generator that walks every page for you vianextCursor. - Fire-and-forget.
create()returns instantly with an id; callwait()later, with gentle backoff polling built in. - Typed errors. Every documented API error maps to a specific error class carrying
code,status, andrequestId. - Wallet safety. Paid POST requests are never auto-retried, so a network blip can't double-charge your wallet. Free idempotent GETs retry automatically on 429/500/503, honoring
Retry-After. - Dual ESM/CJS. Works with both
importandrequire, with bundled, correct TypeScript declarations either way.
Client reference
ASYNCclient.queries.run(prompt, options?)Paid ยท charged from wallet
Sends the prompt, waits for every model plus the consensus, and resolves with one typed QueryResult. The simplest way to use the SDK.
queries.run() blocks for the whole run, so its default timeout is 130 minutes - the server's own worst-case ceiling (125 min) plus headroom. The client budget is deliberately larger than the server's: if the SDK gave up first you would see a timeout on a run that is still going, and still be charged for it. Override with options.timeoutMs. queries.wait() uses the same ceiling. Ordinary calls (retrieve, list, models, wallet, health) time out at 60s.
const result = await client.queries.run("Best commuter motorcycles under $5000?", {
models: ["gpt", "claude", "gemini"],
recursive: false,
});
console.log(result.consensus, result.commonAnswer, result.confidence, result.charged);ASYNCclient.queries.stream(prompt, options?)Paid ยท charged from wallet
Returns an AsyncGenerator<StreamEvent> of typed events as each model responds live. A fatal error SSE event is thrown as the mapped TalkoryError subclass rather than yielded, so you only need to handle model_chunk/model_result/done in the happy path.
for await (const event of client.queries.stream("Compare Python web frameworks in 2026")) {
if (event.type === "model_chunk") {
process.stdout.write(event.chunk);
} else if (event.type === "model_result") {
console.log(`\n${event.modelId} scored ${event.score}`);
} else if (event.type === "done") {
console.log("\n\nConsensus:", event.consensus);
}
}ASYNCclient.queries.create() / .wait(id)Paid ยท charged from wallet
Fire-and-forget pattern: create() starts a run and resolves immediately with an id (202 Accepted), so a dropped connection never loses the run. wait(id) polls retrieve(id) with gentle backoff (starting at 2s, growing ร1.5 up to 10s) until the run leaves "running", up to the same 130 minute default timeout as run().
const run = await client.queries.create("Best commuter bikes under $5000?");
console.log(run.id); // save this - the run continues even if you disconnect
const result = await client.queries.wait(run.id);
console.log(result.consensus);ASYNCclient.queries.consensus(id)Paid ยท idempotent
Generates the consensus and common answer for a query that was run with consensus: false. If the consensus already exists it is returned as-is with charged: 0, so calling it twice never double-charges.
const result = await client.queries.run("Best NAS for home use?", { consensus: false });
const consensus = await client.queries.consensus(result.id);
console.log(consensus.consensus, consensus.confidence);ASYNCclient.queries.retrieve(id) / .list()Free ยท no wallet charge
retrieve(id) fetches a single query by id (used for polling or recovering after a dropped stream). list() is an AsyncGenerator that auto-paginates through your past queries, newest first, walking every page via nextCursor for you.
const query = await client.queries.retrieve("7f9e4c2a-1b8f-4a21-9d3c-e77aa53b6c10");
console.log(query.status);
for await (const q of client.queries.list({ limit: 50 })) {
console.log(q.id, q.prompt, q.charged);
}ASYNChealth.check() / models.list() / wallet.get() / usage.get()Free ยท no wallet charge
Utility calls that never touch your wallet: check live provider health, list available models, read your wallet balance, or pull aggregated usage for your key.
console.log((await client.health.check()).status);
console.log(await client.models.list());
console.log((await client.wallet.get()).balance);
console.log((await client.usage.get({ from: "2026-06-01" })).requests.total);Error handling
Every documented API error maps to a typed error class extending TalkoryError, carrying code, status, message, and requestId, so you can branch with instanceof or log the request id for support.
import { InsufficientBalanceError, RateLimitError, TalkoryError } from "talkory";
try {
const result = await client.queries.run("Best commuter motorcycles under $5000?");
} catch (err) {
if (err instanceof RateLimitError) {
await sleep((err.retryAfter ?? 30) * 1000); // seconds
} else if (err instanceof InsufficientBalanceError) {
// top up at app.talkory.ai
} else if (err instanceof TalkoryError) {
console.error(err.code, err.status, err.requestId);
} else {
throw err;
}
}| Error class | When it happens |
|---|---|
AuthenticationError | Missing or invalid API key. |
InsufficientBalanceError | Wallet has no balance for a paid call. |
InvalidModelError | A model id you sent is unknown, or its provider is not configured on the server. Call models.list() for the current list. |
QueryInProgressError | Too many concurrent queries for this key; wait for one to finish. |
RateLimitError | Too many requests; carries retryAfter in seconds. |
ServiceUnavailableError | Temporary outage or maintenance at api.talkory.ai; retry shortly. |
APIConnectionError / APITimeoutError | Network failure or client-side timeout reaching api.talkory.ai. |
StreamInterruptedError | An SSE stream dropped after the run started; carries queryId to recover the result. |
TalkoryError | Base class for every other API error. |
Security
| Property | Behavior |
|---|---|
| Key resolution | Explicit apiKey constructor option, or TALKORY_API_KEY environment variable. |
| Key redaction | Keys never appear in toString(), console.log, or error messages - shown as tk_live_***a1b2 at most. |
| Transport | HTTPS enforced; plain http:// refused unless allowInsecure: true is explicitly set (local testing only). |
| Telemetry | Zero telemetry. The only outbound traffic is the API calls you make. |
| User-Agent | talkory-js/{version} {runtime} |
FAQ
How do I install the Talkory Node.js SDK?
Run npm install talkory. It requires Node.js 18+ (or any runtime with a global fetch, such as Bun or Deno), has zero runtime dependencies, and ships as dual ESM/CJS with bundled TypeScript declarations.
How does the Talkory Node.js SDK authenticate?
Set the TALKORY_API_KEY environment variable and call new Talkory() with no arguments, or pass an explicit key with new Talkory({ apiKey: "tk_live_xxxxxxxx" }). Keys are redacted from toString(), console.log, and error messages.
Does the Talkory Node.js SDK support streaming?
client.queries.stream(prompt) returns an async generator you consume with for await...of, yielding typed events including model_chunk for live text and a final done event with the consensus.
Does the Talkory Node.js SDK work with both import and require?
Yes. The package ships a dual ESM/CJS build, so both import { Talkory } from "talkory" and const { Talkory } = require("talkory") work out of the box, with correct TypeScript types either way.
Need a key? Head to your API settings. Prefer REST or Python? See the API documentation or Python SDK docs. Package details and release history live on npm โ, and the source is on GitHub โ.