JavaScript and TypeScript

sdk/typescript is a TypeScript client with no dependencies: it uses the fetch built into Node, Deno, Bun and browsers, and Node 22.18+ runs its source directly. Keep the key on your server; never ship it to a browser.

Install it from a clone of https://github.com/jepelasweden/jepela (npm links the folder, and Node runs the TypeScript there):

git clone https://github.com/jepelasweden/jepela
npm install ./jepela/sdk/typescript      # then: import { JepelaClient } from "jepela-sdk"
import { JepelaClient, choice, noul, score } from "./sdk/typescript/src/index.ts";

const jepela = new JepelaClient();                       // JEPELA_API_KEY; the key's region picks the address
const r = await jepela.systemOne(
  { message: "We were charged twice for September again.", account: "ACME Logistics" },
  {
    route: choice("Which team should handle this?", { billing: "Charges and refunds", support: "Product problems", other: "None of these" }),
    refund: noul("Is the customer asking for money back?"),
    urgency: score("How urgent is this?", ["Can wait a week", "Should be handled today", "Blocking the customer now"]),
  },
  { subject: "acme", memory: { compare: true } },
);

r.answers.route.choice;          // typed: "billing" | "support" | "other"
r.answers.refund.noul;           // probability of yes
r.answers.route.answer_confidence;   // the probability of the answer given (see Confidence)
r.memory?.without_memory;        // the same call without the memory
r.warnings;                      // known traps in this request
r.usage.answers_billed;

new JepelaClient({ apiKey, baseUrl, timeoutMs, maxRetries, backoffMs }): the key falls back to JEPELA_API_KEY, the gateway to JEPELA_BASE_URL and then the API of the key's region (see Regions); by default 120 000 ms per call, 2 retries, 500 ms backoff.

Every route has a method: remember, forget, delete, memory, rulesAdd, rules, rulesDelete, pin, exclude, aliases, aliasesDelete, feedback, quality, calibrationFit, calibration, goldenAdd, golden, goldenRun, goldenDelete, decisionsDelete, batchCreate, batchUpload, batch, batches, batchResults, batchWait, finetune, finetunes, finetuneJob, finetuneWait, finetuneDelete, models, usage, and signup(email, { inviteCode }), which needs no key. request(method, path, body) calls any route.

await jepela.memory("acme", true);                   // lines and rules, word for word
await jepela.rulesAdd("mia", "session_result <= -30", "Mia's stop-loss is reached: she stops playing now.", { ttlSeconds: 3600 });
await jepela.pin("acme", "ACME pays for premium support.");
await jepela.exclude("player-9", ["gold_coins"], "Coin bug.", { question: "reward" });
await jepela.aliases("acct-1", ["Globex Corporation"]);
await jepela.aliasesDelete("acct-1", ["Globex Corporation"]);   // those names out again, none added
await jepela.forget("acme", "Ann Lindqvist", { preview: true });   // what would go, and related; nothing removed
await jepela.goldenDelete(["refund-1"]);             // [] deletes nothing; no argument deletes every case
await jepela.request("GET", "/v1/usage");

With memory: { explain: true } the answer says what each memory line did; pins and exclusions show up too:

const e = await jepela.systemOne(state, questions, { subject: "player-9", memory: { explain: true } });
e.memory?.pinned;                // ids of the pinned rules sent
e.memory?.excluded;              // { reward: { gold_coins: "Coin bug." } }
e.memory?.exclusions_skipped;    // exclusions that would have removed every option
e.memory?.explain;               // [{ line, kind: "pinned" | "rule" | "recalled", effect: { question: move } }]
                                 // (and measured_on_first where the second tier gave the answer: see Memory)

Fine-tuning trains a model of your own on your golden cases (at least 20; see Fine-tuning):

await jepela.finetunes();                               // { jobs, min_cases: 20, bases: ["english", "multilingual", "typed-decisions"] }
const job = await jepela.finetune("english");           // { job, model, cases, held_out, state: "running" }
const done = await jepela.finetuneWait(job.job);        // polls finetuneJob(job.job); default 3 600 000 ms, every 10 000 ms
if (done.usable) await jepela.systemOne(state, questions, { model: job.model });
await jepela.finetuneDelete(job.model);                 // its files are deleted and its name stops working

Options on systemOne: model, subject, memory (use, top_k, min_share, compare, placebo, names, question_words, focus, explain; see Request options), robust (true or { orders: 1-5 }, choices of up to 20 options), windows (true or { combine: { question_id: "max" | "mean" | "min" } }), derive, values, cache, method ("auto", the default, matches choices of more than 20 options by vectors and asks the engine the rest; "engine" reads every option; "match" matches every choice), quality ("fast", the default; "balanced" and "best" send the answers the engine is least sure of to a larger, slower second tier: each answer then has right_probability, those answered again tier: 2 and first, and the response quality; see Request options). Each answer has method ("engine" or "match"), and a matched one similarity ({ best, second }). Measured on BANKING77 (600 bank messages, 77 intents) on a GPU test server, 2026-09-27: matching 65.2% right in a median of 12 ms, the engine reading all 77 options 62.2% in 57 ms (Choice).

Errors are classes per status (AuthenticationError, PaymentRequiredError, PermissionDeniedError, NotFoundError, ConflictError, BadRequestError, RateLimitError with retryAfter, EngineError, ServerError, ConnectionFailed). The client retries 429, 502 and 503 with backoff, and a connection that failed before the request left; a decision that may have arrived is never sent again, so it cannot be billed twice.

The source is not type-checked in this repository (tsc is not installed here); tsconfig.json has the settings for checking it. Its tests run against the real engine: python tests/real_steps/test_jepela_typescript_sdk.py.

Without the SDK

const res = await fetch(`${process.env.JEPELA_BASE_URL}/v1/systemone`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.JEPELA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ state, questions, subject }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error.type}: ${data.error.message}`);