Why WOS

Long-term memory for AI agents.

WOS is a memory API. You store a user's memories once, then recall only the relevant ones for each query and pass them to your model's prompt.

Retrieval is purely semantic, with no keyword or BM25 matching, so recall quality is identical across languages. Each query returns a small, bounded context no matter how much you have stored, and no model is ever run over your stored memories.

Core operations

  • store - save a memory for a user.
  • recall - get the relevant memories for a query. This is the main call.
  • search - raw semantic search over stored memories.
  • supersede - update or replace a memory that is out of date.
  • forget - delete a single memory or an entire user (GDPR).
Pick a section on the left for detail on each topic.
Model

Three models, one lineage.

WOS models are named for how people have kept knowledge through history - Tablet, Scroll, Codex. Stone, scroll, bound book: each one does more for your agent than the last.

Tablet

Live
Carved in stone · store & recall

A lean, fast, low-cost way to inscribe and recall memory - the foundation every model builds on.

Scroll

Live
Unrolled · LLM-assisted recall

Adds a language model to read your question more closely and bring back a fuller context, so scattered evidence comes back together rather than one piece short.

Codex

Next
Bound & indexed · self-routing

Opens to the right page on its own - choosing the memory and tools each moment needs, and getting sharper the more it's used.

Tablet 1's full benchmark report is on the benchmark page.

Cost

Pay us $2. Save many times that on your LLM.

WOS feeds your LLM ~1,200 tokens per query - a bounded, relevant slice - instead of stuffing the full history into every prompt. The gap is enormous, and it grows with your history.

LLM cost per 1,000 queries Based on Tablet 1
User history100K
Queries / month1,000
Your LLM
45× cheaper - you save $244/mo
Without WOS$250.00
With WOS$5.50

Every $1 spent on WOS saves ~$98 on the LLM. Bigger history or a pricier model → bigger ROI.

Where the savings come from

  • Without WOS you stuff the whole history into each prompt - 100K tokens × $2.50/1M = $0.25 per query, at GPT-4o input rates (about 2× that on Opus-tier models).
  • With WOS you ingest once ($2/1M), then each query is a tiny retrieve ($3/1M × 1,200) plus your LLM on just ~1,200 tokens.
  • The fewer tokens your LLM reads, the less you pay - and WOS keeps that number flat as memory grows.
Context shrink = history ÷ tokens fed, not cost (the calculator above prices each retrieve).  25K → 21× · 100K → 83× · 200K → 167×.
Multilingual

Every language, the same accuracy.

Retrieval is pure semantic - embeddings only, zero keyword or BM25 matching. So recall quality is identical whether your users write in 日本語, 中文, Español, or English.

Lexical matching like BM25 is tuned to the shape of a particular language - morphology, spacing, script. In a multilingual store that means retrieval quality varies by language. WOS uses no lexical matching at all, so every language goes through the same path.

One store, three languages at once

You don't pick a language per store - mix them freely. Below, one user's memory holds Japanese, English, and Spanish at the same time, and every question finds the right memory regardless of language. This is a real exchange against the live API:

# one user, three languages stored together
mem.add("彼女はコーヒーより紅茶が好き", user_id="alice")                      # Japanese
mem.add("she works at a design studio in Brooklyn", user_id="alice")       # English
mem.add("A ella le encanta hacer senderismo los sábados", user_id="alice")  # Spanish
Actual results - each question crosses into a different language
"¿Qué bebe ella?"               -> 彼女はコーヒーより紅茶が好き
"what does she do on weekends?" -> A ella le encanta hacer senderismo los sábados
"彼女の仕事は?"                  -> she works at a design studio in Brooklyn

No translation step, no language detection, no per-language config. Memories and questions are placed by meaning, not by language - if the meaning matches, the language doesn't matter.

Three languages here is just what fits on a page - there is no supported-language list to be on. The same live test also passes with 中文, Русский, and العربية memories, all verified against the production API.

Why we banned keywords on purpose

Lexical scoring such as BM25 strengthens retrieval for some languages more than others, which gets in the way when one store holds many languages. So we removed it from the engine entirely and enforce that rule in code review: with any lexical scoring in the path, recall quality would differ by language.

LongMemEval is English-only, so it does not measure multilingual recall. The demo above is how you can verify it directly against the live API.
Architecture

No model runs over your memories.

Storage is verbatim and the engine searches by embeddings - cheap, fast, and deterministic. A model is never run over your stored memories. Tablet uses no model at all; Scroll and Codex add one around the engine for stronger results, but it only ever sees your query, never what you stored.

  • Deterministic engine. The engine returns the same memories for the same query, every time - which is why our benchmark variance comes only from the reader model.
  • Cheap at scale. No generation cost to store or retrieve, so your bill tracks storage - not model usage - as memory grows.

Your words, untouched

One common design runs a language model at write time to extract and rewrite "facts" from the text. That design trades three things: generation cost on every write, added latency, and storage of a model's paraphrase rather than the original words. WOS makes the opposite trade - it stores what was said, unchanged, and lets your LLM do the interpreting at read time with the original text in hand.

What WOS is not: not a vector DB you have to operate, and not a RAG framework you have to assemble. No model is ever run over your stored data - that path is pure embeddings. Scroll and Codex do use a language model for stronger results, but it only ever sees your query, never your stored memories - and it never trains on your data or collects it.
Proof

90.7%, measured and reproducible.

90.7% on LongMemEval-S, averaged over 5 independent runs (σ 0.5%, none cherry-picked), graded by the benchmark's canonical GPT-4o judge.

On the same benchmark, scores vary widely with the grading protocol - the judge, the prompt, and what the retrieval layer is allowed to do. We use the benchmark's canonical third-party GPT-4o judge as published, change nothing to fit the test, and publish the scoring code and reader prompt so anyone can reproduce the 90.7% exactly.

The protocol, in one table

ItemWhat we do
DatasetLongMemEval-S (cleaned), ~240K tokens of history per question
JudgeThe benchmark's canonical GPT-4o judge - a third party, not us
Runs5 independent runs, every score published, mean reported (σ 0.5%)
ReaderFixed reader model and prompt, published verbatim

What keeps it honest: a third-party judge, the reader prompt published unchanged, purely semantic retrieval, and every run reported - not just the best. The retrieval engine is deterministic - run it again and you get the same memories.

We climb harder benchmarks

We test on the hardest standard benchmark we haven't yet conquered - and the number is the high-water mark across every WOS model, rewritten each time a better one ships. Clear 94%, and we graduate to a harder benchmark.

LongMemEval-SIn progress
Tablet 185.2%
Scroll 190.7%
GPT-4o judge · best across all WOS models94% to graduate
See the full report
Pricing

Two token rates per model,
plus $0.0001 per request.

Per million tokens plus a flat $0.0001 per request, pay as you go. No subscription, no storage rent, no memory caps. You pay when your agent writes or reads - never for what it remembers.

ModelInput / 1MOutput / 1M
Tablet 1$2$3Live
Scroll 1$4$8Live
Codex 1--TBD
  • $0.0001 per request. A flat fee on every API call, on top of token usage.
  • Storage is free. Ingest pays once; keeping it costs you nothing. No count limit, no retention limit.
  • We store it. We never train on it, use it, or look at it. Your agent's memory is yours - we only organize it so you can retrieve it.
  • Why Tablet is this cheap: its engine runs no model, so our cost is embeddings and disk - not GPUs. Scroll and Codex add a model, which is what their higher price covers.
Other billing models charge monthly for stored volume or cap memory counts by plan. WOS charges nothing for stored data, regardless of volume or age.

Rate limits by usage tier →

For developers

Three calls: store, recall, answer.

One API. The recall() call returns short-term, long-term, and surrounding context in a single round-trip, ready to drop into your prompt.

1

Store

add() facts and turns: your user's words, the assistant's own (speaker "me"), or a person's by name. Embedded on the way in - no LLM.

2

Recall

recall() returns short-term + long-term + context in one call - a bounded, fixed-size context.

3

Answer

Feed that bounded context to your LLM - any provider, your key.

from wontopos import Client
mem = Client(api_key="wos-...")
mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I suggested the jasmine tea", user_id="alice", speaker="me")  # its own words
# one call: short + long + context
ctx = mem.recall("what does alice drink?", user_id="alice")

Memories carry a speaker. Your user's words are the default, speaker "me" stores what the assistant itself said, and a name like "Bob" remembers who around your user said it, so recall can answer by person.

Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.
Quickstart

Your first recall in 5 minutes.

One key, one install line, three calls - your agent has memory. Every snippet on this page was actually run; responses are shown verbatim.

1

Get an API key

Create one in the console. A 155-character key starting with wos-live- is shown once. Keep it in an environment variable - never in code.

2

Install

pip install wontopos        # Python
npm install wontopos        # TypeScript / JavaScript
cargo add wontopos          # Rust
# curl - nothing to install, just set WOS_API_KEY
3

Create a store, then store & recall

A store is the user_id you read and write under. Stores are explicit: create one first (the call below), then store and recall under it. Store - embedded on the way in, no LLM call. Recall - short-term + long-term + context in one round-trip.

from wontopos import Client

mem = Client(api_key="wos-live-...", user_id="alice")  # set the store once
mem.create_store()              # create it (stores are explicit)
mem.add("she prefers tea over coffee")  # no user_id needed

# one call → short-term + long-term + context
ctx = mem.recall("what does alice drink?")
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-...", userId: "alice" });  // set the store once
await mem.createStore();            // create it (stores are explicit)
await mem.add("she prefers tea over coffee");  // no userId needed

// one call → short-term + long-term + context
const ctx = await mem.recall("what does alice drink?");
use wontopos::Client;

let mem = Client::new("wos-live-...").with_user("alice");  // set the store once
mem.create_store(None).await?;            // create it (stores are explicit)
mem.add("she prefers tea over coffee", None, json!({})).await?;

// one call → short-term + long-term + context
let ctx = mem.recall("what does alice drink?", None).await?;
# create the store once - stores are explicit
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store - embedded on the way in, no LLM call
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'

# one call → short-term + long-term + context
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
Actual response - create_store()
{"user_id": "alice", "status": "created"}
Actual response - add()
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
Set the store once. Pass user_id to the client and every call uses it - no need to repeat it; override any single call by passing user_id to it. Stores are explicit: storing into or recalling from a store that doesn't exist returns 404 - create it first. Every account starts with a default store, so with no user_id at all the zero-setup path just works. See Stores to list and manage them.

recall() returns four blocks - short_term (recent turns), long_term (relevant memories), context (what surrounded the best match), and an instruction telling the LLM how to use them. Drop the whole thing into your prompt.

Works in any language. Store in English, ask in Korean, Japanese, or Chinese - the same memory comes back. Embedding search, not keyword matching.

Every method, per language →

Stores

Stores - create, list, delete.

A store is the user_id you read and write under - one isolated memory space per end-user, agent, or topic. Stores are explicit: create one before you store into or recall from it, or the call returns 404. Every account starts with a default store, so you can begin without a create call.

How isolation nests. An account owns workspaces; each workspace isolates its own memory, API keys, and usage (billing is shared at the account). A store lives inside a workspace: keys in the same workspace share its stores, and different workspaces never see each other's memory. account → workspace → store (user_id) → memories.
mem.create_store("alice")        # create (idempotent)
mem.list_stores()              # [{"user_id","created_at"}, ...]
mem.delete_store("alice")        # delete the store + all its memories
await mem.createStore("alice");
await mem.listStores();          // [{ user_id, created_at }, ...]
await mem.deleteStore("alice");     // store + all its memories
mem.create_store("alice").await?;
let stores = mem.list_stores().await?;
mem.delete_store("alice").await?;
# create
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
# list
curl https://api.wontopos.com/api/v1/memory/collections -H "X-API-Key: $WOS_API_KEY"
# delete (store + all its memories)
curl -X DELETE https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
Actual response - create
{ "user_id": "alice", "status": "created" }   // "exists" if it already did
Actual response - list
{ "collections": [
  { "user_id": "default", "created_at": "2026-06-26T02:23:14Z" },
  { "user_id": "alice",   "created_at": "2026-06-26T02:24:01Z" }
], "count": 2 }
Recall on a store that doesn't exist
{ "error": { "type": "not_found_error",
  "message": "Store 'ghost' does not exist. Create it first with
              POST /api/v1/memory/collection {\"user_id\":\"ghost\"}, then store or recall." } }
Use one store per end-user ("alice", "user_42") to keep each person's memory separate, or a single default store for a personal agent. You can also create and browse stores in the console (Memory ids → Issue) without writing code. Deleting a store is permanent - it drops every memory under it.
Recall caching

Repeated recall, at a tenth of the price.

Opt in per request and WOS caches the search result under its query text, with the same prefix rules as LLM prompt caching. While the cache is warm, a repeated or extended query reuses the previous result, and the cached part is billed at 10% of the normal token rate.

Tablet and Scroll only. Caching works on every Tablet and Scroll model, current and future. Codex does not support it: Codex reasons over your memories and learns between calls, so the same question can legitimately come back with a different answer, and a cached result would be wrong by design. Sending cache_control to Codex returns a clear 403.

One conversation, three turns

Here is what actually happens when an agent keeps talking to its memory. Each turn sends the conversation so far as the query, with cache_control on.

writeTurn 1 - "Alice: I moved to Lisbon last spring."

The full query is searched and cached: input at 2x (5-minute TTL).

extendTurn 2 - the same text plus "Bob: How is the weather there?"

Only Bob’s sentence is embedded and searched. The old part costs 0.1x, the new sentence 2x, and the cache now ends at it.

hitTurn 3 - exactly the same query again (a retry, a refresh)

No engine call at all. Everything at 0.1x: the 90% discount.

The rates

OperationToken billingWhat it means
Cache write - TTL 5 minutesThe first request. Its result is kept for 5 minutes, and every read slides the window forward.
Cache write - TTL 1 hourThe first request, kept for a full hour.
Cache read - hit or prefix hit0.1×Every request after the write: the cached part costs a tenth of the normal token rate.

What it saves

A concrete example: your agent sends a 3,000-token conversation as its query and repeats or continues it 10 times within five minutes. Without caching, that is 30,000 input tokens at full price. With a 5-minute cache it is 6,000 for the first write (2x) plus about 2,700 for the nine cached reads: 8,700 billed tokens, 71% less. The longer the conversation runs, the bigger the save.

The prefix rule

Matching is on the front of the query. If the front stays identical and new text is only appended, the cached part is reused and only the new part is searched. If anything before the end of the cached text changes, nothing can be reused.

prefix match
cached    [ A B C D E F G ]

○   [ A B C D E F G ] E
✗   [ B C D E F G ] E

hit - the front is unchanged, E is the only new part
miss - the front changed, so the whole query is searched and cached again

Three rules to remember

  • Extending re-caches through the new tail. After [A B C D E F G] + E, the cache now ends at E: the tail is billed once at the write rate, and the next turn can match all of A..E as its prefix again.
  • One contiguous prefix per request. A query cannot be split into two cached segments; only its front can match.
  • Writes invalidate instantly. Any store, store-turn, bulk-store, forget, supersede, or store deletion drops that store’s cache, so a cached answer can never be stale.

Turning it on

hits = mem.search(
    "...the conversation so far...", user_id="alice",
    cache_control={"ttl": "5m"},   # or "1h"
)
const hits = await mem.search(
  "...the conversation so far...", "alice", 10,
  { cache_control: { ttl: "5m" } },   // or "1h"
);
let hits = mem.search_with(
    "...the conversation so far...", "alice", 10,
    serde_json::json!({"cache_control": {"ttl": "5m"}}),   // or "1h"
).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice",
       "query":"...the conversation so far...",
       "cache_control":{"ttl":"5m"}}'   # or "1h"
Response - the cache object reports what happened
{ "memories": [ ... ],
  "cache": { "status": "hit",              // "write" | "hit" | "extend"
             "ttl": "5m",
             "cache_read_input_tokens": 412,
             "cache_creation_input_tokens": 0 } }

You do not need an SDK for any of this. Caching is one field on one HTTP call, so it works from every programming language. The curl tab is the universal recipe, and the Python, TypeScript, and Rust SDKs are convenience wrappers around the exact same call.

Caching is isolated per store and per model inside your workspace, and it is off by default: without cache_control, nothing about your requests changes.
Who said it

Memory that knows who said it.

People remember by person: what Bob promised, what you said you would do. Tag each memory with a speaker and your agent does the same, on every Tablet and Scroll model.

Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

One team, three memories

A store keeps many voices apart. Register a person once, save each remark under its speaker, then ask by person.

addRegister Bob once: POST /speakers, or add_speaker("Bob") in the SDKs.

The store now knows Bob. The 50-person limit is counted here, at registration; store calls never return a limit error.

BobBob tells your user the deadline moved to Tuesday. Store it with speaker "Bob".

The memory belongs to Bob now: every search that returns it says so.

meYour assistant promises the summary by Friday. Store its own words with speaker "me".

Self-speech gets remembered too, and "me" never counts toward the speaker limit.

askLater: "what did Bob say about the deadline?" Search with speaker "Bob".

Only Bob's words come back. One person's words never come back as someone else's.

Three rules to remember

  • "me" is the assistant itself. Never registered, never counted. Reserved and lowercase: speaker: "Me" or "ME" returns 400 invalid_request_error instead of being silently coerced.
  • The limit lives at registration: 50 per store to start. Registering past it returns 400 invalid_request_error with speaker_limit: 50 in the error body. Storing with an unregistered name also returns 400 and stores nothing. Filtering search by an unregistered name returns 404 not_found_error. Branch on the status and fields, not the message text; we plan to raise the limit.
  • Labels live on every read. Search results, recall's long-term context, and engram results all carry their speaker, so the model always knows whose words it is holding. Pass speaker on a search to get one person's words only. A supersede keeps the speaker; forget removes it.
  • Names are Unicode: any language works. さくら, Иван, and 하늘 are all valid speakers, and attribution behaves identically in every language. Matching is exact after trimming and Unicode normalization, so Bob and bob are two different people. Names cap at 80 characters.
errors - verbatim
# POST /speakers past the limit
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "This store already has 50 registered speakers, ...",
             "speaker_limit": 50 } }

# store with an unregistered name → 400, nothing stored
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "speaker 'Bob' is not registered in this store. Register it first: ...",
             "speaker": "Bob" } }

# search filtered by an unregistered name → 404
{ "type": "error",
  "error": { "type": "not_found_error",
             "message": "speaker 'Bob' is not registered in this store.",
             "speaker": "Bob" } }

Two scoping notes. speaker rides on add / store: add_turn remembers a whole exchange, and per-person labels and the filter come from memories stored with an explicit speaker. And assembled session passages (expand) are composites of several memories, so they carry no label; a speaker filter always returns atomic, labeled memories. And identical content dedups to the first memory: such a store returns status "duplicate" with an explicit note, and no speaker is attached.

We test this the hard way: memories stored with no names in the text, then recalled per person. Attribution comes from the speaker record, not from matching words, so it behaves the same in every language.

Using it

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")
hits = mem.search("what did Bob say about the deadline?", user_id="alice", speaker="Bob")
mem.list_speakers(user_id="alice")
mem.remove_speaker("Bob", user_id="alice")  # memories stay, the tag goes
await mem.addSpeaker("Bob", "alice");  # once per person; "me" needs no registration
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });
const hits = await mem.search("what did Bob say about the deadline?", "alice", 10, { speaker: "Bob" });
await mem.listSpeakers("alice");
await mem.removeSpeaker("Bob", "alice");  // memories stay, the tag goes
mem.add_speaker("Bob", "alice").await?;  # once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;
let hits = mem.search_with("what did Bob say about the deadline?", "alice", 10, json!({"speaker": "Bob"})).await?;
mem.list_speakers("alice").await?;
mem.remove_speaker("Bob", "alice").await?;  // memories stay, the tag goes
curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about the deadline?","speaker":"Bob"}'

curl "https://api.wontopos.com/api/v1/memory/speakers?user_id=alice" -H "X-API-Key: $WOS_API_KEY"

curl -X DELETE https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # memories stay, the tag goes
response
{ "memories": [
    { "content": "Bob said the deadline moved to Tuesday",
      "speaker": "Bob", ... } ] }
GET /speakers
{ "user_id": "alice",
  "speakers": [ { "speaker": "Bob", "memories": 2, "created_at": "2026-07-10T04:20:39Z" } ],
  "count": 1, "limit": 50 }

list shows who the store knows with per-person memory counts against the limit. remove unregisters a person: their memories stay, only the name tag goes.

Add-on · Beta

MCP - memory for AI tools

The core of WOS is the API and SDKs. The MCP server is an add-on on top of them: the same memory, plugged into tools you did not build - Claude Code, Claude Desktop, Cursor.

One install line gives the agent nine memory tools it uses on its own. And because memory lives in your account, what one tool writes, every other tool recalls - including agents you build on the SDK.

What you can do with it

  • Claude Code that remembers your project. Decisions, bug fixes, preferences - recalled next session without re-explaining anything.
  • Start in ChatGPT, continue in Claude. Same store, same memory - the conversation crosses tools instead of restarting.
  • Your own agent stays in the loop. What Claude Code learns, an SDK agent recalls - and what your agent stores, Claude Code recalls back.

Works in Claude Code, Claude Desktop, Cursor, Windsurf, and any MCP host. ChatGPT reaches the same memory through Actions plus the OpenAPI spec.

Install

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp

The agent gets nine tools - recall · remember · search · update · forget · list_memories · engram · stats · create_store - each described so it knows on its own when to use them.

The add-on itself is free and open on npm - you pay only the normal usage pricing for the API calls it makes. Needs Node 18+ and an API key from the console.

Open the developer page

Add-on

OpenAPI spec

The complete machine-readable map of the API - every endpoint, request, response, and error.

OpenAPI is the industry-standard format for describing an HTTP API in a file machines can read.

https://api.wontopos.com/openapi.json

What you can do with it

Postman: File → Import → paste the URL, and all 16 endpoints appear as a clickable collection. ChatGPT: create a GPT, add an Action, paste the same URL. Codegen: openapi-generator -i .../openapi.json -g go builds a client in a language we do not ship.

Import it into Postman, generate a client in a language we do not ship, wire up ChatGPT Actions, or run contract checks in CI. A test pins it to the live routes, so it cannot drift.

Add-on

llms.txt

The whole API as one page of text an AI can read.

llms.txt is a web convention: one plain-text page at the site root that tells an AI everything it needs about a product.

https://wontopos.com/llms.txt

Drop it into your IDE or coding agent and it knows how to build on WOS - auth, endpoints, patterns, errors. Updated with every release.

Same facts as the OpenAPI spec, different audience: the spec is precise structure for tools, this file is prose an AI (or a person) reads in one pass. Both update with every release.

Model Context Protocol · Beta

Your memory inside every AI tool

One command gives Claude Code, Claude Desktop, Cursor, or any MCP host a long-term memory backed by your WOS account. No integration code - the agent gets nine memory tools and decides when to use them.

MCP is in beta. The nine tools work today and are tested, but the surface may still change while we finish it. The API and SDKs underneath are stable and versioned.

Install

Claude Code - one line (create a key in the console first):

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick which store it remembers into (optional): add --env WONTOPOS_USER_ID=my-project
# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# Claude Desktop and any other MCP host
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →  ·  Add to VS Code →

Optional env: WONTOPOS_USER_ID picks the default store, WONTOPOS_MODEL the engine, WONTOPOS_BASE_URL a self-hosted deployment. WONTOPOS_READ_ONLY=1 switches to read-only (recall/search/list only).

Before you share one store

  • Use a dedicated key. Keys carry their workspace, so a key made just for MCP scopes what connected tools can ever touch - and you can rotate it in the console without touching your app's keys.
  • Read-only mode. WONTOPOS_READ_ONLY=1 registers no write tools at all: the agent can recall, search, list memories, run engrams, and read stats, but cannot store, update, forget, or delete. Right for agents that should consult memory, not own it.
  • Keep tool confirmation on. MCP hosts ask before running tools by default - leave that on for forget in particular, since deletes are shared by every tool on the store.
  • Anything stored is recallable by every tool holding the key. Never store secrets - API keys, passwords - as memories.
  • Recalled memories are data, not instructions. The tool descriptions tell the agent this explicitly. Still, don't store untrusted third-party text as memories in a store an autonomous agent obeys.
  • Deletes are shared too. A forget or delete_all from one tool erases for all of them.
  • "me" means whichever agent writes the store. If several agents share one store, their "me" voices merge. Give each agent its own store (WONTOPOS_USER_ID) for separate identities.
  • One account pays. Every connected tool draws from the same balance and rate limit.

Then just talk

youremember that we ship on Fridays

The agent calls the remember tool. Stored durably in your store - the session ending changes nothing.

new sessionwhen do we ship?

A fresh session has zero chat history. The agent calls recall and answers from memory: Fridays.

Things to say

  • "This repo uses pnpm, remember that" → remember stores it; the next session already knows.
  • "What error format did we settle on last week?" → recall pulls the decision back into context.
  • "Actually, the deadline moved to Friday" → the agent sees it contradicts what it recalled and calls update to fix that memory in place, keeping the history.
  • "That's wrong, forget it" → the agent finds the memory id and calls forget - your host asks for confirmation first.
  • "What do you remember about me?" → list_memories pages through everything stored, so the agent can answer or tidy up.

There is nothing special to phrase - these are plain sentences, not commands. The agent reads each tool's description and picks on its own.

The nine tools

  • recall - One-call context: recent turns plus relevant long-term memories. Its description tells the agent to call it first whenever past context matters.
  • remember - Store a durable fact or decision. speaker: "me" marks the agent's own words; a registered name marks who said it.
  • search - Semantic search, with an optional per-person speaker filter.
  • forget - Delete one memory by id.
  • speakers - Register, list, or unregister the people a store remembers.
  • create_store - Stores are explicit - one per end-user, project, or agent.

SDK or MCP?

  • The SDK goes inside an app you are writing. Your code decides exactly when to store and what to recall - deterministic, typed, versioned. Building a product? Use the SDK.
  • MCP plugs into an AI tool you did not write. The agent decides when to use memory, guided by the tool descriptions - zero code. Right for Claude Code, Claude Desktop, Cursor, or giving a finished assistant a memory.

Same API, same stores underneath - an app built on the SDK and a Claude Code session on MCP share one memory. Pick per surface, not either-or.

One memory across every tool

Memory belongs to the account, not the tool. The same store written from ChatGPT (Actions plus the OpenAPI spec) recalls in Claude Code and in your own agents, and back - a conversation started in one tool continues in another.

And because it is one store, you can leave Claude Code and keep talking where you build: an SDK agent with the same key and store recalls everything Claude Code just learned - and what your agent stores, Claude Code recalls next session.

Runs locally over stdio (npx wontopos-mcp): your key stays in your environment and never passes through a third-party server. It wraps the TypeScript SDK, so automatic retries, redirect refusal, and key masking apply as-is.
Model Context Protocol · Beta

Claude Code

The flagship path: one command in your terminal, and every session starts with memory.

  1. Create an API key in the console. A key carries its workspace, so one key = one memory space.
  2. Register the server. --scope user makes it available in every project; without it, only the current project sees it.
  3. Check it: run /mcp inside Claude Code - wontopos should be listed with nine tools.
  4. Make it automatic: one line in your CLAUDE.md - "when past context matters, call wontopos recall first" - and every session starts with memory without being asked.
claude mcp add wontopos --scope user \
  --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick a store (optional): add --env WONTOPOS_USER_ID=my-project
Model Context Protocol · Beta

Claude Desktop

Add the block below to claude_desktop_config.json (Settings → Developer → Edit Config), restart the app, and the nine tools appear. Note: claude.ai on the web and mobile needs a remote MCP server, which WOS does not offer yet - the desktop app is the supported path.

# claude_desktop_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

Cursor

Add the block below to ~/.cursor/mcp.json - or press the one-click button - and restart Cursor. The agent picks up the nine tools.

# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →

Model Context Protocol · Beta

VS Code

VS Code (Copilot agent mode) reads MCP servers from .vscode/mcp.json in the project - add the block below or press the one-click button.

# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to VS Code →

Model Context Protocol · Beta

Windsurf

Windsurf (Cascade) reads ~/.codeium/windsurf/mcp_config.json: add the block below and reload - the same nine tools appear.

# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

ChatGPT

ChatGPT's MCP connectors only accept remote servers, so the supported path today is a custom GPT with an Action: create a GPT, add an Action, paste the OpenAPI spec URL below, and set your API key as the auth header. The GPT then calls the same memory your other tools use.

# GPT → Configure → Actions → Import from URL
https://api.wontopos.com/openapi.json
# Authentication: API Key · Header name: X-API-Key

Same store, same memory: what ChatGPT stores through the Action, Claude Code recalls through MCP - and back.

Model Context Protocol · Beta

Gemini CLI

Gemini CLI reads MCP servers from ~/.gemini/settings.json: add the block below, restart the CLI, and the same nine tools appear there too.

# ~/.gemini/settings.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Python SDK

Python - every method, three groups.

Write, read, delete. Every example below was run against the live API on 2026-07-10; responses are verbatim.

pip install wontopos
from wontopos import Client

mem = Client(api_key="wos-live-...")  # or read from an env var

Choose a model

The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default on the client; override a single call by passing model=.

mem = Client(api_key="wos-live-...", model="tablet-1")  # default engine
mem.recall("...", user_id="alice")                  # tablet-1
mem.recall("...", user_id="alice", model="scroll-1")  # or pick a model per call

list_models ✓ live-tested

The catalog - the ids you can pass to model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.

mem.list_models()
Actual response
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping ✓ live-tested

Confirm connectivity and that your API key works - a one-line setup check.

mem.ping()   # True, or raises AuthenticationError / PaymentRequiredError

The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.

Write

add ✓ live-tested

Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.

mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")  # its own words - no registration needed
Actual response
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn ✓ live-tested

Store one conversation turn (user + assistant) into short-term and long-term memory at once.

mem.add_turn("hi", "hello!", user_id="alice")
Actual response
{"status": "ok"}

speaker ✓ live-tested

Every memory can carry who said it. Register a person once, then pass their name as the speaker; "me" (the assistant's own words) never needs registration. Search accepts a speaker too, so you can recall one person's words only.

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", user_id="alice", speaker="me")
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.search("what did Bob say about deadlines?", user_id="alice", speaker="Bob")
response
{"memories": [{"content": "Bob said the deadline moved to Tuesday", "speaker": "Bob", ...}]}
Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

add_bulk ✓ live-tested

Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.

mem.add_bulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", user_id="alice")
Actual response
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update ✓ live-tested

A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.

mem.update("576700aa-...", "she switched to coffee this year", user_id="alice")
Actual response
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

Read

search ✓ live-tested

Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.

r = mem.search("what does she drink?", user_id="alice", limit=1)
Actual response (HTTP body)
{"memories": [{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }], "search_ms": 315, "total_found": 1}
FieldMeaning
similarityRaw embedding similarity to your query (0–1).
is_supersededTrue if this fact was replaced by update().
search_msServer-side retrieval time.

recall ✓ live-tested

One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.

ctx = mem.recall("what does she drink?", user_id="alice")
Actual response (shape - lists shortened)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history ✓ live-tested

Recent conversation turns (short-term memory), oldest first.

turns = mem.history("alice")
Actual response (HTTP body)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats ✓ live-tested

Memory counts for one user.

mem.stats("alice")
Actual response
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get ✓ live-tested

Fetch one memory by id - the id that add or list_memories returned. The original text plus its metadata, never the vector. An id from another store, or a deleted or invalidated one, returns 404.

m = mem.get("alice", memory_id="576700aa-...")
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "source_type": "user_said",
 "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}

list_memories ✓ live-tested

List a store's memories - the original text you stored plus its metadata, never the vector. Paged by cursor: pass the returned next_cursor back for the next page.

page = mem.list_memories("alice", limit=100)
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "source_type": "user_said",
    "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}
 ]}

iter_memories · export_memories

Page through every memory with no cursor bookkeeping, or pull the whole store at once.

for m in mem.iter_memories("alice"):   # every page, no cursor bookkeeping
    print(m["id"], m["content"])
everything = mem.export_memories("alice")   # the whole store as a list

Delete

delete ✓ live-tested

Delete a single memory by id.

mem.delete("alice", memory_id="576700aa-...")
Actual response
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all ✓ live-tested

Erase everything for one user - one call, GDPR-ready.

mem.delete_all("alice")
Actual response
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

Errors and reliability

Every failure is a typed error - catch the specific one (rate limit, auth, payment) or all of them with the base WosError.

from wontopos import PaymentRequiredError, NotFoundError
try:
    mem.add("...", user_id="alice")
except NotFoundError:
    mem.create_store("alice")   # store didn't exist yet
except PaymentRequiredError:
    top_up()                       # out of credit - don't retry

rate_limit

Read the quota left after any call and slow down before you hit the limit.

mem.search("...", user_id="alice")
rl = mem.rate_limit   # {"limit": 150, "remaining": 3, "reset": ...}
TypeScript SDK

TypeScript - every method, three groups.

Write, read, delete. Every example below was run against the live API on 2026-07-10; responses are verbatim.

npm install wontopos
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-..." });

Choose a model

The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default in the constructor; override a single call with withModel().

const mem = new Client({ apiKey: "wos-live-...", model: "tablet-1" });  // default
mem.recall("...", "alice");                          // tablet-1
mem.withModel("scroll-1").recall("...", "alice");  // or pick a model per call

listModels ✓ live-tested

The catalog - the ids you can pass to model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.

await mem.listModels();
Actual response
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping ✓ live-tested

Confirm connectivity and that your API key works - a one-line setup check.

await mem.ping();   // true, or throws AuthenticationError / PaymentRequiredError

The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.

Write

add ✓ live-tested

Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.

await mem.add("she prefers tea over coffee", "alice");
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });  // its own words - no registration needed
Actual response
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

addTurn ✓ live-tested

Store one conversation turn (user + assistant) into short-term and long-term memory at once.

await mem.addTurn("hi", "hello!", "alice");
Actual response
{"status": "ok"}

speaker ✓ live-tested

Every memory can carry who said it. Register a person once, then pass their name as the speaker; "me" (the assistant's own words) never needs registration. Search accepts a speaker too, so you can recall one person's words only.

await mem.addSpeaker("Bob", "alice");  # once per person; "me" needs no registration
await mem.add("I promised to send the report on Friday", "alice", { speaker: "me" });
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.search("what did Bob say about deadlines?", "alice", 10, { speaker: "Bob" });
Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

addBulk ✓ live-tested

Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.

await mem.addBulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", "alice");
Actual response
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update ✓ live-tested

A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.

await mem.update("576700aa-...", "she switched to coffee this year", "alice");
Actual response
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

Read

search ✓ live-tested

Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.

const r = await mem.search("what does she drink?", "alice", 1);
Actual response (HTTP body)
{"memories": [{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }], "search_ms": 315, "total_found": 1}
FieldMeaning
similarityRaw embedding similarity to your query (0–1).
is_supersededTrue if this fact was replaced by update().
search_msServer-side retrieval time.

recall ✓ live-tested

One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.

const ctx = await mem.recall("what does she drink?", "alice");
Actual response (shape - lists shortened)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history ✓ live-tested

Recent conversation turns (short-term memory), oldest first.

const turns = await mem.history("alice");
Actual response (HTTP body)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats ✓ live-tested

Memory counts for one user.

await mem.stats("alice");
Actual response
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get ✓ live-tested

Fetch one memory by id - the id that add or list_memories returned. The original text plus its metadata, never the vector. An id from another store, or a deleted or invalidated one, returns 404.

const m = await mem.get("alice", "576700aa-...");
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "source_type": "user_said",
 "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}

listMemories ✓ live-tested

List a store's memories - the original text you stored plus its metadata, never the vector. Paged by cursor: pass the returned next_cursor back for the next page.

const page = await mem.listMemories("alice", { limit: 100 });
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "source_type": "user_said",
    "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}
 ]}

iterMemories · exportMemories

Page through every memory with no cursor bookkeeping, or pull the whole store at once.

for await (const m of mem.iterMemories("alice")) console.log(m.id, m.content);
const everything = await mem.exportMemories("alice");

Delete

delete ✓ live-tested

Delete a single memory by id.

await mem.delete("alice", "576700aa-...");
Actual response
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

deleteAll ✓ live-tested

Erase everything for one user - one call, GDPR-ready.

await mem.deleteAll("alice");
Actual response
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

Errors and reliability

Every failure is a typed error - catch the specific one (rate limit, auth, payment) or all of them with the base WosError.

import { NotFoundError, PaymentRequiredError } from "wontopos";
try {
  await mem.add("...", "alice");
} catch (e) {
  if (e instanceof NotFoundError) await mem.createStore("alice");
  else if (e instanceof PaymentRequiredError) topUp();   // out of credit
  else throw e;
}

rateLimit

Read the quota left after any call and slow down before you hit the limit.

await mem.search("...", "alice");
const rl = mem.rateLimit;   // { limit: 150, remaining: 3, reset: ... }
Rust SDK

Rust - every method, three groups.

Write, read, delete. Every example below was run against the live API on 2026-07-10; responses are verbatim.

cargo add wontopos
use wontopos::Client;

let mem = Client::new("wos-live-...");

Choose a model

The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default with with_model(); chain it again to override a single call.

let mem = Client::new("wos-live-...").with_model("tablet-1");  // default
mem.recall("...", "alice").await?;                       // tablet-1
mem.with_model("scroll-1").recall("...", "alice").await?;  // or pick a model per call

list_models ✓ live-tested

The catalog - the ids you can pass to with_model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.

mem.list_models().await?;
Actual response
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping ✓ live-tested

Confirm connectivity and that your API key works - a one-line setup check.

mem.ping().await?;   // Ok(true), or Err whose .kind() is Auth / PaymentRequired

The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.

Write

add ✓ live-tested

Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.

mem.add("she prefers tea over coffee", "alice", json!({})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;  // its own words - no registration needed
Actual response
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn ✓ live-tested

Store one conversation turn (user + assistant) into short-term and long-term memory at once.

mem.add_turn("hi", "hello!", "alice").await?;
Actual response
{"status": "ok"}

speaker ✓ live-tested

Every memory can carry who said it. Register a person once, then pass their name as the speaker; "me" (the assistant's own words) never needs registration. Search accepts a speaker too, so you can recall one person's words only.

mem.add_speaker("Bob", "alice").await?;  # once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", "alice", json!({"speaker": "me"})).await?;
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.search_with("what did Bob say about deadlines?", "alice", 10, json!({"speaker": "Bob"})).await?;
Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

add_bulk ✓ live-tested

Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.

mem.add_bulk("Alice moved to Brooklyn in March...", "alice", "general").await?;
Actual response
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update ✓ live-tested

A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.

mem.update("576700aa-...", "she switched to coffee this year", "alice").await?;
Actual response
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

Read

search ✓ live-tested

Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.

let r = mem.search("what does she drink?", "alice", 1).await?;
Actual response (HTTP body)
{"memories": [{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }], "search_ms": 315, "total_found": 1}
FieldMeaning
similarityRaw embedding similarity to your query (0–1).
is_supersededTrue if this fact was replaced by update().
search_msServer-side retrieval time.

recall ✓ live-tested

One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.

let ctx = mem.recall("what does she drink?", "alice").await?;
Actual response (shape - lists shortened)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history ✓ live-tested

Recent conversation turns (short-term memory), oldest first.

let turns = mem.history("alice").await?;
Actual response (HTTP body)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats ✓ live-tested

Memory counts for one user.

mem.stats("alice").await?;
Actual response
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get ✓ live-tested

Fetch one memory by id - the id that add or list_memories returned. The original text plus its metadata, never the vector. An id from another store, or a deleted or invalidated one, returns 404.

let m = mem.get("alice", "576700aa-...").await?;
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "source_type": "user_said",
 "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}

list_memories ✓ live-tested

List a store's memories - the original text you stored plus its metadata, never the vector. Paged by cursor: pass the returned next_cursor back for the next page.

mem.list_memories("alice", 100, None).await?;
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "source_type": "user_said",
    "created_at": "2026-07-10T04:20:39Z", "event_date": null, "is_superseded": false}
 ]}

list_all_memories

Page through every memory with no cursor bookkeeping, or pull the whole store at once.

let all = mem.list_all_memories("alice").await?;   // every page, collected

Delete

delete ✓ live-tested

Delete a single memory by id.

mem.delete("alice", "576700aa-...").await?;
Actual response
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all ✓ live-tested

Erase everything for one user - one call, GDPR-ready.

mem.delete_all("alice").await?;
Actual response
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

Errors and reliability

Every failure is a typed error - catch the specific one (rate limit, auth, payment) or all of them with the base WosError.

use wontopos::ErrorKind;
match mem.search("...", "alice", 10).await {
    Ok(hits) => { /* use hits */ }
    Err(e) if e.kind() == ErrorKind::NotFound => { mem.create_store("alice").await?; }
    Err(e) if e.is_rate_limited() => { /* back off */ }
    Err(e) => return Err(e),
}

rate_limit

Read the quota left after any call and slow down before you hit the limit.

mem.search("...", "alice", 10).await?;
let rl = mem.rate_limit();   // Some(RateLimit { remaining: Some(3), .. })
curl

curl - no install, same methods.

No SDK to install - any HTTP client works. Set your key once and call the same endpoints the SDKs wrap. Base URL https://api.wontopos.com, auth via X-API-Key, JSON in and out.

# set your key once (never hard-code it)
export WOS_API_KEY="wos-live-..."

Write

store ✓ live-tested

Store one memory. Embedded on the way in - no LLM call.

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'
Actual response
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

store-turn ✓ live-tested

Store one conversation turn (user + assistant) into short and long-term memory at once.

curl -X POST https://api.wontopos.com/api/v1/memory/store-turn \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","user_msg":"hi","assistant_msg":"hello!"}'
Actual response
{"status": "ok"}

speaker ✓ live-tested

Every memory can carry who said it. Register a person once, then pass their name as the speaker; "me" (the assistant's own words) never needs registration. Search accepts a speaker too, so you can recall one person's words only.

curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"I promised to send the report on Friday","metadata":{"speaker":"me"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about deadlines?","speaker":"Bob"}'
Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

supersede ✓ live-tested

A fact changed - the old memory is marked superseded, the new one takes its place in recall.

curl -X POST https://api.wontopos.com/api/v1/memory/supersede \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","old_memory_id":"576700aa-...","new_content":"she switched to coffee this year"}'
Actual response
{"new_memory_id": "07e94433-...", "old_memory_id": "576700aa-...", "status": "superseded"}

Read

search ✓ live-tested

Semantic search, most relevant first. Pure embedding - any language finds any memory.

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does she drink?","max_results":1}'
Actual response
{"memories": [{"id": "576700aa-...", "content": "she prefers tea over coffee",
   "similarity": 0.63, "is_superseded": false}], "search_ms": 315, "total_found": 1}

recall ✓ live-tested

One round-trip returns short-term + long-term + context + an instruction. Paste it straight into your prompt.

curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does she drink?"}'
Actual response (shape)
{"short_term": {"count": 2, "turns": [...]},
 "long_term":  {"count": 4, "memories": [{"content": "she prefers tea over coffee", "similarity": 0.63}]},
 "context":    {"count": 4, "around_top_memory": ["[match] she prefers tea over coffee"]},
 "instruction": "Use short_term for recent context, long_term for relevant past memories..."}

Delete

forget ✓ live-tested

Delete one memory by id, or omit it to delete everything for a user (GDPR).

curl -X POST https://api.wontopos.com/api/v1/memory/forget \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'  # omit memory_id = delete all
Actual response
{"memories_deleted": 1, "status": "deleted", "user_id": "alice"}

Every endpoint + body fields →

Engrams

Engrams

Callable recall tools your model can invoke - each one a different retrieval strategy over the same memory. Use one, or run several at once.

Live now. The general engrams below are no-LLM retrieval pipelines, so they run on every tier from Tablet 1 up. Memoir and Archive, a separate model mode, are covered in their own section below.

More engrams ship regularly - this list grows.

Memoir & Archive Scroll 1.2+

This one is a delivery form, not a callable tool. On Scroll 1.2 and up, pick it per call - form: "memoir" or form: "archive" - and that recall, a plain search included, comes back with time written that way.

All engrams Engrams

Time_awareness Scroll 1.2+

A delivery form - pick it per call. Pass form - memoir or archive - with any call on a form-capable model (Scroll 1.2 and up), and the response comes back rendered that way: a plain search, a recall, or any engram. In the SDKs it is a form field, like tz; over HTTP it is the X-WOS-Form header. A Memoir reads the way a person remembers; an Archive keeps an exact record - the difference shows up most in how each writes time.

Memoir

form: "memoir"
Remembered like a person · a narrative

Tells what happened and how one moment led to the next, with the soft sense of time a person recalls - read as experience, not a list.

Archive

form: "archive"
Kept as a record · precise time

Returns matches as exact records - precise elapsed time and absolute anchors, structured for a model to read straight off.

It renders memories you've already stored - it doesn't create them. Each memory is one store / add call under a user_id (that user_id is that person's store). Store first; then any recall - the plain search below included - comes back time-tagged. See Quickstart to store.
# plain search - no engram - the memoir form on Scroll 1.2
r = mem.search("what does Alice drink?", user_id="alice", model="scroll-1.2", form="memoir", tz=9)
# every hit gets a .time field → "a couple weeks ago"  (form="archive" → "2 weeks ago (Jun 09)")
// plain search - no engram - the memoir form on Scroll 1.2
const r = await mem.withModel("scroll-1.2").search("what does Alice drink?", "alice", 10, { form: "memoir", tz: 9 });
// search_with merges extra fields into the body - form picks the render, tz the local clock
let r = mem.with_model("scroll-1.2").search_with("what does Alice drink?", "alice", 10, json!({"form": "memoir", "tz": 9})).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: wos-live-..." -H "X-WOS-Model: scroll-1.2" -H "X-WOS-Form: memoir" -H "X-WOS-Timezone: 9" \
  -d '{"user_id":"alice","query":"what does Alice drink?"}'
# each memory comes back with a "time" field; use X-WOS-Form: archive for exact time

tz is the caller's UTC offset in hours - so "this morning" and the 4am day boundary land in their local time. Omit it for UTC; over HTTP it's the X-WOS-Timezone header. Roughly, by region: US East -5, US Central -6, US West -8 · UK / Lisbon 0 · Central Europe +1 · Eastern Europe +2 · India +5.5 · China / Singapore +8 · Korea / Japan +9 · Sydney +10. (Standard time - daylight saving shifts some regions by +1; pass whatever your users are actually on.)

Same search, two forms - the memories are identical, only time changes:

Result · form: memoir
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "a couple weeks ago" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday afternoon" },
  { "content": "Alice moved to Brooklyn",          "time": "about half a year ago" }
] }
Result · form: archive
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "2 weeks ago (Jun 09)" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday at 14:00" },
  { "content": "Alice moved to Brooklyn",          "time": "6 months ago (Dec 2025)" }
] }
ElapsedMemoirArchive
3 mina few minutes ago3 minutes ago
14 minabout 15 minutes ago14 minutes ago
30 minhalf an hour ago30 minutes ago
50 minabout an hour ago50 minutes ago
2 hra couple hours ago2 hours ago, at 13:10
8 hrthis morning8 hours ago, at 07:10
yesterday pmyesterday afternoonyesterday at 14:00
last nightlast night17 hours ago, at 22:00
2 daysa couple days ago2 days ago (Tue 15:10)
6 daysseveral days ago6 days ago (Fri 15:10)
9 daysabout a week agolast week (Jun 16)
16 daysa couple weeks ago2 weeks ago (Jun 09)
35 daysabout a month agolast month (May 21)
60 daysa couple months ago2 months ago (Apr 2026)
180 daysabout half a year ago6 months ago (Dec 2025)
380 daysabout a year agolast year (Jun 2025)
800 daysa couple years ago2 years ago (Apr 2024)
1500 daysabout 4 years ago4 years ago (May 2022)

Every value above is the renderer's real output. Look at the two "yesterday" rows: a Memoir splits afternoon from last night - a day is one sleep - while an Archive writes a single clock time and draws no day or night line.

How each mode reads time

Memoir - the way people actually say it. Recent moments stay fairly sharp (about 15 minutes, half an hour), then the wording widens the further back you go - a couple weeks, about half a year, a couple years - the way memory itself loosens with distance. Inside a day it drops the clock for a landmark: this morning, last night, yesterday afternoon. And a day is one sleep, not a calendar tick: the boundary sits around 4am local time, so a late night still reads as the same evening, not already tomorrow.

Archive - precise, always with an anchor. Every line carries the exact elapsed time plus an absolute reference a model can compute from, and the anchor tightens as it nears: a clock for today (8 hours ago, at 07:10), a weekday and clock this week (2 days ago (Tue 15:10)), a date this month (last week (Jun 16)), a month and year beyond (6 months ago (Dec 2025)). Never vague, never wrong.

Memoir and Archive render the whole recall - a plain search, a recall, or an engram. The model tier (Tablet → Scroll → Codex) sets how much the engine does; the form (memoir / archive) sets how it writes time. Available on Scroll 1.2 and up.
All engrams Engrams

deep_recall

Multi-hop recall. Searches your query, then takes the top match and searches again on its content - pulling in linked context a single search would miss. Best when memories reference each other (a person → their projects → details). Returns up to ~12.

out = mem.engram("deep_recall", "what should I know about Alice?", user_id="alice")
const out = await mem.engram("deep_recall", "what should I know about Alice?", "alice");
let out = mem.engram("deep_recall", "what should I know about Alice?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"deep_recall","user_id":"alice","query":"what should I know about Alice?"}'
Response
{ "engram": "deep_recall", "hops": 2, "count": 12,
  "memories": [ ... ],
  "usage": { "input_tokens": 5, "output_tokens": 61 } }
Metered by tokens - every call returns usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.
All engrams Engrams

timeline

Time-ordered recall. Returns memories sorted newest-first by when the event happened, not by relevance. For "when did X", history, and sequence questions. Returns up to 15.

events = mem.engram("timeline", "project milestones", user_id="alice")
const events = await mem.engram("timeline", "project milestones", "alice");
let events = mem.engram("timeline", "project milestones", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"timeline","user_id":"alice","query":"project milestones"}'
Response
{ "engram": "timeline", "hops": 1, "count": 15,
  "memories": [ ... ],
  "usage": { "input_tokens": 4, "output_tokens": 88 } }
Metered by tokens - every call returns usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.
All engrams Engrams

gather

Broad gather. Searches, then expands around the top three matches - a wider net than deep_recall. Use it to pull in everything related to a person, project, or topic in one call. Returns up to ~18.

related = mem.engram("gather", "everything about Project Atlas", user_id="alice")
const related = await mem.engram("gather", "everything about Project Atlas", "alice");
let related = mem.engram("gather", "everything about Project Atlas", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"gather","user_id":"alice","query":"everything about Project Atlas"}'
Response
{ "engram": "gather", "hops": 4, "count": 18,
  "memories": [ ... ],
  "usage": { "input_tokens": 6, "output_tokens": 142 } }
Metered by tokens - every call returns usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.
HTTP API

Every endpoint, one base URL.

No SDK required - any HTTP client works. Base URL https://api.wontopos.com, auth via the X-API-Key header, JSON in and out. Memory ops are POST; managing stores uses POST / GET / DELETE on /collection. A store must exist first (see Stores) or in-store ops return 404.

EndpointPurposeBody fields
POST /api/v1/memory/collectioncreate a storeuser_id
GET /api/v1/memory/collectionslist your stores(none)
DELETE /api/v1/memory/collectiondelete a store + its memoriesuser_id
/api/v1/memory/storestore one memoryuser_id · content · metadata? (speaker)
/api/v1/memory/store-turnstore a conversation turnuser_id · user_msg · assistant_msg
POST /api/v1/memory/speakersregister a speaker (explicit, up to 50)user_id · speaker
GET /api/v1/memory/speakerslist registered speakers + memory countsuser_id
DELETE /api/v1/memory/speakersunregister a speaker (memories stay)user_id · speaker
/api/v1/memory/bulk-storebackfill a text blobuser_id · content · category?
/api/v1/memory/searchsemantic searchuser_id · query · max_results? · speaker?
/api/v1/memory/recallshort + long + contextuser_id · query
/api/v1/memory/historyrecent turnsuser_id
/api/v1/memory/statsmemory countsuser_id
/api/v1/memory/supersedereplace a changed factuser_id · old_memory_id · new_content
/api/v1/memory/forgetdelete one (or all)user_id · memory_id? (omit = delete all)
# create the store once (stores are explicit)
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store a memory
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'

# recall - one call, ready for your prompt
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
Actual response - store
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
Usage tiers

Same features for everyone.
Tiers only raise your limits.

Every tier runs the full engine - same recall quality, same languages, every method. Tiers advance automatically through Tier 5 as your cumulative credit purchases grow, with no application or sales call. Enterprise (Tier 6) is the one exception.

Spend limits

Each tier caps how much you can spend per calendar month. You advance immediately when your cumulative credit purchases reach the next threshold.

Usage tierCredit purchaseMonthly spend limit
Tier 1$5$100
Tier 2$40$500
Tier 3$200$1,000
Tier 4$400$5,000
Tier 5$1,000$25,000
Tier 6 - EnterpriseTalk to usNo limit

Rate limits

Rate limits are per account - every API key on an account shares one limit, which scales with your tier. Exceeding it returns a 429 with a retry-after header; back off (1s → 2s → 4s) and retry. Every endpoint is idempotent-friendly, so retries are safe.

TierRequests per minute
Tier 1150
Tier 2300
Tier 3600
Tier 41,500
Tier 53,000
Tier 6 - EnterpriseCustom

Enterprise (Tier 6) gets custom rate limits, an SLA, dedicated support, and an optional self-host license - talk to us.

Pricing is usage-based: tokens plus a flat $0.0001 per request. Tablet is $2 per 1M input tokens, $3 per 1M output. Storage is free with no caps. See why we price it this way.
Errors & limits

When something goes wrong.

Errors come back as a JSON envelope with a stable type, a human message, and a request_id you can send us when reporting an issue.

Actual response - invalid key (HTTP 401)
{"type": "error", "error": {
   "type": "authentication_error",
   "message": "Invalid or revoked API key.",
   "request_id": "063f8b83-eee2-4383-a5cf-11e4bcd29d7c"
 }}
HTTPMeaningWhat to do
401Invalid or revoked API keyCheck the key; issue a new one in the console.
422Malformed body (missing/wrong-type field)The message names the exact field - fix and retry.
429Rate limitedBack off exponentially (1s → 2s → 4s) and retry. Safe: all endpoints are idempotent-friendly.
5xxServer-side problemRetry with backoff; include request_id if you contact us.
# SDK error handling (Python)
from wontopos import Client, WosError

try:
    mem.search("...", user_id="alice")
except WosError as e:
    if e.status == 401: ...  # bad key
    elif e.status == 429: ...  # back off and retry
Key safety. Your key is shown once at creation and stored only as a hash on our side. Keep it in an environment variable; if it leaks, revoke it in the console - revocation is immediate.

Rate limits are per account, shared across all your keys, and scale with your tier - see Usage tiers. Your account's usage is shown in the console.

Self-host

Your servers, your data.

The engine can run inside your own infrastructure - same API, same SDKs. Point the client at your host and nothing else changes.

mem = Client(api_key="...", base_url="https://wos.your-host.com")
const mem = new Client({ apiKey: "...", baseUrl: "https://wos.your-host.com" });
let mem = Client::with_base_url("...", "https://wos.your-host.com");
  • Data residency. Memories never leave your network.
  • Same surface. The same methods and endpoints work identically.
  • Licensing. Self-host packages are arranged per deployment - contact us.
Scale

Beyond the context window.

WOS recalls from histories of 1.4M tokens - far larger than any LLM context window - and still hands back a tight ~1,470-token slice.

Your agent's memory isn't capped by what fits in a prompt. It keeps everything and retrieves only what matters, no matter how large the history grows.

Privacy

Private, and yours.

Your data stays in your store. We never train on it, view it, or reuse it - we only organize it so you can retrieve it.

  • BYOK. Your LLM key is sent per request and never stored.
  • Isolated. Memories are scoped per account, then per user_id.
  • GDPR delete & self-host. One call wipes a user; run the engine in your own environment if you prefer.