Talkory Node.js SDK

Ask once, hear from up to 5 LLMs (GPT, Claude, Gemini, Grok, Perplexity), and get back an AI consensus - fully typed, zero runtime dependencies. 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
DetailValue
Node.js18+ (also works on Bun, Deno, or any runtime with global fetch)
DependenciesNone at runtime
Module formatsESM (import) and CommonJS (require), both typed
LicenseMIT

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 run

queries.run() waits for every model plus the consensus before resolving (can take a few minutes); its default timeout is 600 seconds.

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" });
Keep your key secret. Call Talkory only from backend code, never from a browser or other client-side code where users could read it. The SDK never reads or writes keys to disk, and redacts them everywhere - 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.

If the connection drops mid-run, the SDK throws 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.

OptionTypeDefaultDescription
modelsstring[]all plan modelsSubset of model ids from models.list() (max 10). Omit to run every model on your plan.
countrystring"global"ISO country code (e.g. "in") to localise answers.
recursivebooleanfalseRound-2 self-review: each model critiques and improves its own answer before consensus.
consensusbooleantruefalse skips consensus generation (cheaper); generate it later with queries.consensus().
const models = await client.models.list(); // free

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);

Key features

  1. Async-first, no callbacks. Every call is a Promise or AsyncGenerator - await a result, or for await...of a stream.
  2. Auto-paginating history. client.queries.list() is an async generator that walks every page for you via nextCursor.
  3. Fire-and-forget. create() returns instantly with an id; call wait() later, with gentle backoff polling built in.
  4. Typed errors. Every documented API error maps to a specific error class carrying code, status, and requestId.
  5. 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.
  6. Dual ESM/CJS. Works with both import and require, 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. Default timeout is 600 seconds (overridable via options.timeoutMs).

Example
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.

Example
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 a 15 minute default timeout.

Example
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.

Example
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.

Example
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.

Example
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 classWhen it happens
AuthenticationErrorMissing or invalid API key.
InsufficientBalanceErrorWallet has no balance for a paid call.
InvalidModelErrorUnknown model id, or model not on your plan.
QueryInProgressErrorToo many concurrent queries for this key; wait for one to finish.
RateLimitErrorToo many requests; carries retryAfter in seconds.
APIConnectionError / APITimeoutErrorNetwork failure or client-side timeout reaching api.talkory.ai.
StreamInterruptedErrorAn SSE stream dropped after the run started; carries queryId to recover the result.
TalkoryErrorBase class for every other API error.

Security

PropertyBehavior
Key resolutionExplicit apiKey constructor option, or TALKORY_API_KEY environment variable.
Key redactionKeys never appear in toString(), console.log, or error messages - shown as tk_live_***a1b2 at most.
TransportHTTPS enforced; plain http:// refused unless allowInsecure: true is explicitly set (local testing only).
TelemetryZero telemetry. The only outbound traffic is the API calls you make.
User-Agenttalkory-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 โ†—.