MemTree API
Context Memory
With Context Memory, LLMs are no longer limited by their context window. Coding sessions, agents, and conversations can now continue indefinitely as context is compressed via a combination of hierarchical GraphRAG and summarization.
Coding
Models will now get better, rather than worse, as your coding sessions stretch into the millions of tokens.
Agents
Agents can now try many approaches, learning from their past mistakes and remembering important lessons, no matter how long ago they happened.
Chat
Keep your storytelling, role-play, therapy, medical, or other long-running threads going without worrying about the model forgetting important details or missing out on overarching themes.
Motivation
Humans can recall arbitrarily long events at arbitrary levels of detail. For example, people can provide a two minute or one hour version of stories that took place over a few hours to eons. Cognitive scientists refer to this ability as scale-invariant temporal memory, a key function of episodic memory. Episodic memory and semantic memory together form the two main pillars of long-term memory in people.
Many current LLM memories, like ChatGPT's memory and Cursor Memories, are examples of semantic memory. But without episodic memory, LLMs are forced to ingest the entirety of their experience at full resolution for every token they produce. In practice, this leads to context windows ballooning to contain too much detail and models producing slop. Then when threads are restarted or compacted, models must be reinformed with key details from previous threads. Many approaches use RAG to recall details, but they often recall poorly sized chunks and/or lose out on the benefits of summarization for understanding how those chunks fit into the larger context of the thread.
How It Works
Context Memory can be thought of as a hierarchical episodic memory that retains source excerpts alongside lossy summaries of long message histories. In our implementation, Context Memory is structured as a B-tree where the top of the tree contains a high level summary of the current message history and the bottom of the tree contains verbatim excerpts from that history that are relevant to recent messages. In between are summaries that get more detailed as you go towards the leaves. Rather than simply return matching chunks of text without context, as is typical with RAG, we retrieve relevant details contextualized within a tree of summaries. It's a specialized form of GraphRAG where relationships between nodes are encoded in the tree structure. This tree structure allows us to efficiently expand and collapse nodes based on relevance at query-time.
Example memory response when asking about a divide by zero error
- Feature 1 summary
- Task 1 summary
- Task 2 summary
- Step 1 summary
- Step 2 summary
Divide by zero error details
- Task 3 summary
Imagine that you were working to resolve a divide by zero error in a codebase, and that the only source verbatim relevant to the error is:
Divide by zero error details
By unfolding the tree and returning the path of summaries above retreived excerpts, we can provide a concise overview of what led to this divide by zero error.
When we compress: the whole-request budget
Compression is a cost, not a goal: it trades verbatim conversation for a lossy summary. So every request is measured against a whole-request budget — the system prompt, tools, memory, and recent messages all count — and we only compress when the request exceeds it.
- Under budget: the conversation is returned unchanged (after light sanitization such as stripping
<think>tags) with"compressed": false. Forward your original request and your provider's prompt cache stays intact. Indexing still runs in the background so the memory is warm for the first turn that crosses the budget. - Over budget: older messages are replaced by the memory (see above), the most recent messages are kept verbatim, and the response carries
"compressed": true. Compression can only happen once a completed index exists; an over-budget request whose index is still building is returned uncompressed with"compressed": falseand the job is started.
The budget is chosen per request, in this order of precedence:
compression_target_tokensif you send it.-1disables compression entirely: every call passes through uncompressed, still sanitized and still indexed.- Otherwise a policy for the
modelyou send: 800k tokens for the 1M-context Anthropic models that hold up across the window (Claude Fable 5 / 5.1, Opus 5.5, Opus 5, Opus 4.8); 550k for Sonnet 5.5 and Sonnet 5 and 400k for Sonnet 4.6, where long-context retrieval degrades before the window; half of the measured effective context for other benchmarked models (e.g. Opus 4.5); 50k tokens for everything else, including models we do not recognize. - Otherwise, with no
model, the 50k static default.
The budget is always capped at model_context_limit. It is a soft target: the most recent message is never truncated, so a very large final message can push a compressed request past it. The comparison is done in characters at a conservative ~2.7 characters per token, so a request can fit your model's window in tokens and still be over a window-sized target — use compression_target_tokens: -1, not a huge target, when you want a guarantee of no compression.
You may set compression_threshold_tokens separately to delay compression until that size, while keeping a smaller target. An optional client_input_tokens measurement above the effective threshold also triggers a compression attempt when the character estimate fits. Neither field overrides compression_target_tokens: -1.
Soft vs. hard. compression_target_tokens is a soft target: we compress toward it, but the most recent message is never truncated and can push the result past it. model_context_limit is the hard cap: it is the only thing that produces a 413, and the target is always clamped to it. Send the model's real window as model_context_limit and the size you want conversations compressed to as compression_target_tokens; sending a small target as the hard cap turns every over-target conversation into a 413 wait instead of a compression.
What stays verbatim
Compression replaces indexed history with memory and retains recent messages where they fit. Under hard-window pressure, older un-indexed messages may also be truncated; the most recent message is preserved, or the request fails with 413:
- Messages not yet indexed are normally retained; see indexing for hard-window truncation and waiting. Indexing runs in the background and trails the newest messages, so a burst of new content (for example a long agent tool loop) can remain verbatim until the index catches up, subject to that hard-window limit. Indexing duration varies with the conversation and provider; until the first index exists, an over-budget request comes back with
"compressed": false, and calling again sooner cannot shrink it. - The most recent message is always returned in full and never summarized. If it alone is larger than
model_context_limit, the request fails with a 413. If it is only larger than the budget, it is sent anyway and the result exceeds the budget. - Extended thinking. For Anthropic models, the messages from the last preserved thinking block onward are kept in full, as the API requires inside a tool loop.
- Tool calls are never split from their results.
Reading a MemTree
Every conversation's memory is a MemTree: a hierarchy of summaries whose leaves point at the exact transcript lines they summarize. You can read the tree a request was compressed with, either whole or through small query endpoints that return only what you ask for.
Finding the tree
Each /context_memory response, compressed or passthrough, carries an X-Polychat-Memtree-Url header: the request's MemTree page, e.g. https://app.polychat.co/m/2g36dGG1een5. For partner keys it is present only when a memory tree is ready to view for that response: show “View memory” whenever it is present. It is absent until a conversation's first tree is built, without an end_user_id, and it can be missing in conversations containing something shaped like a secret (stored copies are redacted). The last path segment is the page id: a 12-character link id, with -s appended when the page shows the tree the request was served from. Treat it as opaque. The header is optional: index_only responses and errors (including 413s) have none, so don't require it. /usage/memtree/<request id> is the same page, and every endpoint below accepts the link id, the full request id, or older 12-hex-digit ids under either prefix. Opened in a browser, the URL shows the tree; the endpoints below are the machine forms of the same URL. Ignore X-Polychat-Usage-Url if you see it: it links our internal usage dashboard.
A request's own tree is built in the background after its response. Until it is ready, these URLs answer with the tree the request was compressed against, marked "served_prefix": true (its node ids change once the request's own tree lands, so do not cache them across that switch). When there is no tree to show yet, they answer 202, or 410 once there never will be one (see below).
Building (202) vs. unavailable (410)
Every MemTree URL (the page, its .json, and the search, nodes, lines and messages reads) answers one of two ways when it has no tree to show:
- 202
{"status": "building", "retry_after_seconds": 15}with aRetry-Afterheader: the tree is still on its way. Wait that many seconds and retry. - 410
{"status": "unavailable", "reason": …, "detail": …, "request_id": …}: final. This request's tree will never be built, so do not retry; open a newer page of the same session instead. There is noRetry-After. When the owner reads a page from a Claude Code session, the body also carrieslinks.session: that session's page list (below).reasonis one of:failed: indexing failed and will not be retried.content_filtered: the indexing model's safety filter blocked the conversation.superseded: a newer request's job covered these messages before this one ran.stalled: the indexing job never finished and is no longer retried.not_indexed: no tree was built for this request (too few messages to index, already covered by another request's tree, or the tree expired).expired: a partner's tree past its retention ("detail": "This memory has expired.", with no link to a newer page).
GET /usage/memtree/sessions/{session_id}.json (owner only; share links cannot list) returns a bounded list of MemTree pages of one Claude Code session, newest first, each with its links.html and links.json. Its separate newest_tree field links the completed tree with the largest recorded message count in that session, including trees named by unbilled index-only requests. Both are scoped to the owner and, for pooled partners, the established end_user_id.
Auth
- Authorization:
Bearer <API key>, the same key you send to/context_memory(x-api-keyalso works). A key reads only the requests it made: another account's request is a 403, an unknown one a 404, and no valid key a 401 whose body says how to authenticate. - Share links (
?share=<token>, created by the owner on the usage dashboard) work on every endpoint below. A share made without the verbatim source exposes summaries only:blocksis empty, search matches summaries only, and/linesis a 404.
end_user_id from its own user identity, never trust an arbitrary client-supplied value, and check request ownership before forwarding reads. Tail and related-tree lookups use the end user recorded on the request. Session lookups require your server to pass the established end_user_id query parameter; without it, pooled accounts receive no session or related-tree links. An end-user id is a partner assertion, not a separate credential.
The whole tree: GET /m/{id}.json
One JSON document with the full tree and, where allowed, the transcript it indexes. Good for tooling that walks the tree locally; for a long session it runs to several MB, so prefer the query endpoints below when you need a few nodes.
- nodes: a flat list; node
0is the root. Each node hasid, its summarys, and its children's idsk.h, when present, is the content hash of a sealed (immutable) subtree. - Leaves also carry
l = [block, start_line, end_line]: a 1-based, inclusive line range ofblocks[block], the raw transcript text that leaf summarizes. Summaries are lossy; the leaf lines are ground truth. A higher block, then a later line, is later in the conversation. - blocks: the transcript, as a list of text blocks. Empty when the tree was shared without its source or the blocks were never archived;
source_notethen says which. - tail (when present): recorded messages after this tree, from the latest accepted job's snapshot. Some may already be covered by a newer tree: each message's
stateisin_newer_treeornot_indexed, andnot_indexed_fromgives their boundary (null if all are covered). The live session's newest <10k tokens may be absent until the next accepted job or final index. The block holdsnote,source_messages_hash,source_status,start,end,count, andmessages. Each message hasi(position),k(kind),x(text, truncated and marked if oversized), and optionallyat(time). Inline limits are 1,000 messages and about 1,000,000 characters of text, preferring the newest;omitted_beforegives the first included position. Read other positions with/messages. Only owners and include-source shares receive tail content. - instructions: a short guide to the format for an agent reading the document.
- Metadata:
request_id,messages_hash(the tree's identity),model,created_at,tree_created_at,message_count,session_id,links, andserved_prefixwhen applicable.
Query endpoints
The same operations the MemTree tools in Claude Code Infinite give its agents, run on our side. Each takes the same auth as the .json, returns JSON, and starts with request_id and messages_hash (plus served_prefix when applicable) so you can tell which tree answered. Bad parameters are a 400 with a detail message; an unknown node or block is a 404. These reads are not billed.
GET /m/{id}/search?q=&limit=
Lexical search over every node's summary and every leaf's transcript lines. q is split on whitespace into lowercase terms with surrounding punctuation trimmed; common words (the, is, what, …) are dropped, and a query with no terms left is a 400. A term matches as a case-insensitive substring. Hits rank by how many distinct terms a node covers, then by how many terms a single line holds, then by summary matches and total occurrences; ties go to the later node. limit defaults to 10 and is clamped to 1–50. A leaf hit carries up to 3 snippets (whitespace collapsed, a window of about 240 characters around the first match); summaries are cut at 600 characters.
For the owner, the same terms are also matched against the messages after the tree that no tree covers yet (the .json tail, often how a session ended). Matching messages come back as tail_hits (at most 5, most terms first, then newest): each has the message position i (read it whole with /messages?start=i&end=i), its kind k, state, at when recorded, matched_terms and up to 3 snippets with the line number within the message. tail gives the tail's start, end and not_indexed_from. Both are absent when nothing in the tail matches, and for shares.
curl -H "Authorization: Bearer $API_KEY" \
"https://api.polychat.co/m/3f2a9c1b7e40/search?q=deploy+target&limit=5"
{
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"messages_hash": "9b1c…",
"query": "deploy target",
"terms": ["deploy", "target"],
"limit": 5,
"hits": [
{
"id": 6,
"leaf": true,
"depth": 2,
"summary": "Deploy target and cache invalidation",
"range": {"block": 1, "start": 2, "end": 4},
"matched_terms": ["deploy", "target"],
"line_hits": 2,
"score": 250.5,
"snippets": [
{"line": 2, "text": "assistant: the Deploy target is cloud-run"},
{"line": 3, "text": "assistant: Cache invalidation on DEPLOY; deploy deploy"}
]
}
]
}
range is null for a branch; depth is the distance from the root. Pass a hit's range to /lines for the exact text, or its id to /nodes for its context.
GET /m/{id}/nodes/{node_id}
One node: its full summary, its leaf range if it is a leaf, the path from the root to it, and its children. Start at node 0 to browse top-down.
{
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"messages_hash": "9b1c…",
"id": 4,
"leaf": false,
"depth": 1,
"summary": "Cache work",
"range": null,
"path": [{"id": 0, "summary": "Session about deploys and caching"}],
"children": [
{"id": 5, "leaf": true, "summary": "Cache key choice", "range": {"block": 1, "start": 1, "end": 1}},
{"id": 6, "leaf": true, "summary": "Deploy target and cache invalidation", "range": {"block": 1, "start": 2, "end": 4}}
]
}
GET /m/{id}/lines?block=&start=&end=
The exact transcript lines start–end (1-based, inclusive) of block block, all three required. end past the block's last line is clipped; start past it is a 400. One call returns at most 400 lines and about 40,000 characters, always whole lines except a single first line longer than that, which is cut and marked … [line truncated: N chars]. When a cap stops the read early, next_start is where to continue; end in the response is the last line returned.
{
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"messages_hash": "9b1c…",
"block": 1,
"start": 2,
"end": 4,
"block_line_count": 4,
"lines": [
"assistant: the Deploy target is cloud-run",
"assistant: Cache invalidation on DEPLOY; deploy deploy",
"tail line without terms"
],
"next_start": null,
"truncated": false
}
GET /m/{id}/messages?start=&end=&full=
Recorded messages after this tree (the .json tail), including messages already covered by a newer tree, by position: start–end are inclusive values of i, both optional. Without start you get the newest messages, the end of the session being what an agent usually wants. With start the range fills forward; without it, the newest messages that fit are kept. Results are returned in chronological order. One call returns at most 50 messages and about 40,000 characters of text; oversized messages are cut and marked. A tool call or tool result longer than 2,000 characters or 40 lines is shortened to its first and last lines, with shortened: true on the message, a marker … [N lines / M chars omitted; full=true returns it whole] where text was left out, and a shortened_note in the response; user and assistant text is never shortened. Add full=true to get every message whole (the .json tail takes the same parameter). Each message has a state of in_newer_tree or not_indexed. The response carries tail_start and tail_end (the whole tail's range), start and end (what was returned) and messages, after the usual request_id and messages_hash. A wholly out-of-range request or a negative position is a 416; a partially overlapping range is clipped, and a non-integer position is a 400. Only the owner and shares made with the verbatim source can read it: a summaries-only share, or a tree with no recorded tail, gets a 404. The tail comes from the latest accepted job, not necessarily the complete live conversation: its newest <10k tokens may be absent until the next accepted job or final index. The .json inline tail keeps at most 1,000 messages and about 1,000,000 characters of text, preferring the newest; omitted_before identifies omitted older messages, which can be read by position. The page uses the same bounded snapshot. not_indexed_from marks the boundary between messages in a newer tree and messages in no tree yet (null when all are covered).
curl -H "Authorization: Bearer $API_KEY" \
"https://api.polychat.co/m/3f2a9c1b7e40/messages?start=2050&end=2051"
{
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"messages_hash": "9b1c…",
"note": "Not yet indexed: these messages came after this tree and are in no tree yet. …",
"tail_start": 2050,
"tail_end": 2051,
"start": 2050,
"end": 2051,
"messages": [
{"i": 2050, "k": "user", "x": "open the PR", "at": "2026-10-06T01:00:00Z"},
{"i": 2051, "k": "assistant", "x": "PR #42 opened"}
]
}
Finding sessions
Two endpoints look across all of your MemTree sessions at once: list them, and search the transcript text of every tree. A session is a Claude Code session (the id Claude Code resumes with). Requests from other clients are grouped by conversation (the same first message), or stand alone when no tree was built for them. Both endpoints return JSON, newest first or best first, and page with a cursor.
- Owner only. Authenticate with your API key (
Authorization: Bearer <API key>) or the dashboard cookie. Every result is one of your own requests; nothing from another account is ever listed or searched. Share links open one page, never a list: a share token here is a 403, and no valid key a 401. - Paging. Each response carries
next_cursor, an opaque signed token; pass it back unchanged ascursorwith the same query and filters for the next page. It isnullon the last page. Cursors bind the owner, partner end user, mode, query, filters and ranking positions. Altered, unsigned or mismatched cursors return 400; they cannot authorize a free vector page. Every page still requires owner authentication. No copy of the result set is stored. - Filters (both endpoints):
sinceanduntilare ISO 8601 times or dates (UTC when no offset is given) and select requests made in[since, until); counts and times in the results cover those requests.projectmatches the working directory name or the git repository (owner/repo, or justrepo), case-insensitively; Claude Code Infinite records these with each request starting with the release that adds itslist_sessionstool; older requests have no project.qis limited to 500 characters andprojectto 200.
end_user_id, the same opaque per-user id recorded in x-client-meta on its /context_memory calls; without it the finder returns 403. The id must contain 1–128 printable ASCII characters. Rows without that id are excluded. Ordinary owner accounts cannot supply this parameter (400). Shared partner credentials are not enabled merely by supplying an id.
GET /v1/memtree/sessions?since=&until=&project=&q=&cursor=&limit=
Your sessions, most recently active first. q keeps sessions whose title, first message or project contains it (case-insensitive substring). limit defaults to 20 and is clamped to 1–100. Free. The signed cursor retains the first page's request-time cutoff so newly arriving requests do not move an unseen session ahead of the cursor. Start without a cursor to include newer activity; tree completion and other live state can still change.
curl -H "Authorization: Bearer $API_KEY" \
"https://api.polychat.co/v1/memtree/sessions?project=polychat&since=2026-09-01&limit=2"
{
"sessions": [
{
"id": "6025e1f7-074b-4abb-a8e7-dbf07ef1e81f",
"kind": "claude_code_session",
"session_id": "6025e1f7-074b-4abb-a8e7-dbf07ef1e81f",
"title": "Usage dashboard overhaul and MemTree token tracking",
"snippet": "Restyle the usage page to look like the MemTree page…",
"first_at": "2026-09-28T17:02:11.412000+00:00",
"last_at": "2026-09-29T01:40:57.018000+00:00",
"project": {"dir": "polychat", "repo": "acme/polychat", "branch": "main", "commit": "c6fe251"},
"models": ["claude-opus-5-5"],
"request_count": 212,
"tokens": {"prompt": 18400311, "completion": 0, "conversation": 164220},
"latest_tree": {
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"ref": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"links": {
"url": "https://api.polychat.co/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"html": "/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"json": "/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own.json"
}
},
"ending": {
"at": "2026-09-29T01:40:51.200Z",
"message_count": 1840,
"last_user": "ship it and open the PR",
"actions": ["Bash", "Bash", "Edit", "Bash"],
"last_assistant": "PR #212 is open: restyled usage page, MemTree token column…"
}
}
],
"next_cursor": "<signed cursor>",
"limit": 2
}
id: the session's key, stable across pages.kindisclaude_code_session(thensession_idis Claude Code's session id),conversation(another client's conversation) orrequest(a lone request with no tree).title: the root summary of the session's newest finished tree (the main thread's, not a sub-agent's);nullwhen no tree was built.snippet: the start of the conversation's first message.tokens.prompt/completion: billed tokens over the listed requests;tokens.conversation: the size of the conversation the latest tree covers.latest_tree: that tree's page; read it with the endpoints above.nullwhen there is none. Itsrefpins the indexing style and the request's own or served tree. Pass this reference as ccc'streeargument, or follow the supplied links, to keep reading the same tree after indexing finishes. The original UUID remains inrequest_id. Pinned references require owner authentication; share tokens cannot use them.ending: how the session ended, from its newest stored record (the latest accepted indexing job, so its newest <10k tokens may not be there yet):last_user, the last message the person typed (up to 240 characters; Claude Code's own side-request prompts are skipped);actions, up to 6 tools called after it, oldest first;last_assistant, the start of the last assistant text after it;at, the time of the newest message (or of the record). Text is redacted like the rest of the list.nullwhen no record your account wrote is found; partner keys never get one.
GET /v1/memtree/search?q=&mode=text|vector&tree=&project=&since=&until=&cursor=&limit=
Searches the transcript lines (the leaves) of every tree in your sessions. Each passage shows once, from the newest tree of its conversation (same session or conversation and indexing style) that stores it: an older leaf is dropped when a newer tree's leaf has its stored lines, either all of them or all but a short tail the older tree alone had (at most a third of its lines, or 20 lines; Claude Code side requests leave such tails). In text mode that tail must hold none of the query's words, so nothing only the older copy shows is hidden. Divergent histories, leaves holding a redacted secret and other conversations remain separate. Deduplication runs after every ownership and provenance filter. q is required. limit defaults to 10 and is clamped to 1–50. Search pages read live data: newly indexed leaves may appear after the cursor, including new copies of previously seen transcript text. Unchanged embedding rows do not repeat across vector pages.
tree (a request id or a pinned reference such as <request id>-v1-own) limits the search to that one tree, in either mode. A leaf’s line range must be a node of the requested tree. Explicit provenance, when recorded, authorizes only its listed source namespaces and overrides legacy lineage checks; unknown explicit provenance denies the leaf. A legacy leaf without explicit provenance requires its source to belong to the requested tree’s own prefix-hash lineage. These are the same provenance rules used by unfold. Other leaves are removed before ranking and pagination. It searches your own trees only: a tree you do not own, including one shared with you by link or ?share= token, and any tree= search without a valid key, gets the same 404 as a page that does not exist (and counts toward the per-IP failed-lookup limit, 429, like the pages). To search a shared page, use its own /m/<id>/search, which applies the share's rules. It answers 409 while the tree is still building, and ignores project, since and until. Cursors are bound to it. Partner keys must also supply end_user_id matching the row’s client_meta.end_user_id, or receive 403. Scoped responses confirm the requested reference in tree.
mode=text(default, free): full-text search. Words match whole (not as substrings), case-insensitively, with no stemming; all words must appear."quoted phrase"matches words in order,orbetween words matches either, and-wordexcludes a word. Identifiers likeresolve_viewermatch as a phrase of their parts; a path likesrc/proxy.tsmatches whole. Hits are ranked by term frequency relative to leaf length.snippetshows the matched words wrapped in<b>…</b>. Only the 500 most recent matching leaves are ranked;"matches_capped": truesays a query hit that limit, so add words or narrow by time or project. A query too broad to finish in 8 seconds (typically a quoted phrase of common words) is a 422 asking for a more specific query.mode=vector: semantic search. A request without a cursor starts a new billed search, charging one query-embedding receipt per model, including cache hits. A valid signed continuation is free even if another server must re-embed the query, using Polychat's provider credentials. Caching only saves provider work. Similarity scores from different embedding models are not comparable, so results come ingroups, one per model, each ranked on its own; there is no merged list. Accounts that need to pay first get a 402 on the first page; valid continuations and text search remain available. An empty search with no embedding models does not charge.- Vector ordering. Each model continues strictly after its last
(distance, row id). Continuation pages use exact distances and have no 4,000-row prefix cap. Large-account first pages can use approximate HNSW search; that first page may omit better matches ordered before its final cursor. Deduplication keeps the newest copy wherever it ranks, checked against every eligible leaf, so pages agree. This is live search, not a frozen result set.
curl -H "Authorization: Bearer $API_KEY" \
"https://api.polychat.co/v1/memtree/search?q=%22cache+invalidation%22+deploy&project=polychat&limit=1"
{
"query": "\"cache invalidation\" deploy",
"mode": "text",
"limit": 1,
"hits": [
{
"id": "6025e1f7-074b-4abb-a8e7-dbf07ef1e81f",
"kind": "claude_code_session",
"session_id": "6025e1f7-074b-4abb-a8e7-dbf07ef1e81f",
"tree": {
"request_id": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17",
"ref": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"created_at": "2026-09-29T01:38:02.211000+00:00",
"links": {
"url": "https://api.polychat.co/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"html": "/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own",
"json": "/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own.json",
"lines": "/usage/memtree/3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own/lines?block=1&start=2&end=4"
}
},
"leaf": "leaf_node_1:2:4.txt",
"range": {"block": 1, "start": 2, "end": 4},
"snippet": "the Deploy target is cloud-run … Cache invalidation on DEPLOY",
"score": 0.0913,
"node": 7,
"address": "3f2a9c1b-7e40-4d2a-9a51-0c8e2b6f4d17-v1-own#7",
"path": [
{"id": 0, "summary": "Deploys and caching for the API"},
{"id": 4, "summary": "Cache work"}
]
}
],
"next_cursor": "<signed cursor>",
"matches_capped": false
}
range is the leaf's [block, start, end] in that tree, as in the .json; tree.links.lines reads those exact lines. address names the matched node as <tree ref>#<node id>, and path lists its ancestors from the root with short summaries; /usage/memtree/<tree ref>/nodes/<node id> opens the node, its children and its path (whose ids lead to parents and siblings). A hit whose node cannot be located has no address and keeps its range. Per-tree search (/usage/memtree/<id>/search) hits carry the same address and path. Use tree.ref as the ccc tree argument to preserve that exact tree and its line ranges. A vector search answers with groups in place of hits:
{
"query": "how did we pick the deploy target",
"mode": "vector",
"limit": 10,
"groups": [
{
"embedding_model": "voyage-3.5",
"has_more": true,
"hits": [
{"id": "6025e1f7-…", "kind": "claude_code_session", "session_id": "6025e1f7-…",
"tree": {"request_id": "3f2a9c1b-…", "links": {"lines": "/m/3f2a9c1b7e40/lines?block=1&start=2&end=4", "…": "…"}},
"leaf": "leaf_node_1:2:4.txt", "range": {"block": 1, "start": 2, "end": 4},
"snippet": "assistant: the Deploy target is cloud-run…", "score": 0.8826,
"embedding_model": "voyage-3.5"}
]
}
],
"next_cursor": "<signed cursor>",
"charged": true
}
score is cosine similarity within its model's group. charged is true for a first-page embedding receipt, including cache hits, and false for a continuation or a search with no embedding models.
Search SQL statements have an 8-second timeout and return 422 when that limit is reached, asking for a more specific query (typically a quoted phrase of common words; add a rarer word or narrow by time, project or tree). A vector operation still running after a 30-second wait returns 502; paid embedding work remains tracked until it finishes, including after a disconnected client. First-page usage is recorded even if ranking fails; continuation errors remain uncharged. A retry without a cursor starts another billed search, although it may reuse the provider result. Missing server cursor-signing configuration returns 503 before paid work.
⚡️ Speed
Retrieval uses embeddings and vector similarity search to find relevant excerpts and summaries, then unfolds their paths in the tree. Latency depends on request size, index state, provider latency, and database load; no fixed response-time guarantee is implied.
API
We build on the OpenAI compatible API format where the message history is passed in its entirety for every request. The main input is a list of OpenAI compatible messages. The output messages are a compressed version of the input messages.
The majority of older input messages get condensed into a single user message containing the context memory. The recent messages are then returned after the system message and memory messages.
Semantic Addressing
There is no memory ID as the messages themselves serve to identify the memory. This allows branching, reverting, and incrementally updating memories while critically avoiding the indexing of messages that are no longer in the message history. E.g. if a coding session goes down a wrong path and the messages are reverted to an earlier state or if a chat edits a message, branches into a multiple conversations, or clones a previous message history. This means that if previous messages are modified (except for system messages: see below), a new memory will be built at the point of the modification.
Indexing
Indexing happens in the background; duration varies with input size, structure, retries, and provider latency. We do not block API requests to wait for indexing unless we are unable to fit recent unindexed messages inside the whole-request budget. We may attempt to truncate unindexed messages using an exponential decay algorithm that prioritizes recent messages to avoid blocking. However, we will never truncate the most recent message as it's critical for the model to see it in its entirety. If the conversation does not fit in the model_context_limit and no memory exists yet, we start indexing and block on it (up to 5 minutes), returning the compressed conversation as soon as the index completes. If indexing fails, or is still running after 5 minutes, we return an HTTP 413 whose detail carries an estimated indexing time; retry after that.
Agent loops that make many tool calls per user turn can keep the index warm cheaply with index_only: it triggers indexing for the messages sent and returns immediately with no compression and no output messages. It bills only the un-indexed tail it hands to an indexing job (see pricing).
A new indexing job starts only once more than 10,000 un-indexed tokens have built up, so the last stretch of a conversation would otherwise stay out of every tree. When a conversation ends, send one last index_only call with final_index: true to index that tail even below the minimum.
Chunking
Inputs are chunked semantically at index time. This ensures chunks are semantically cohesive and that boundaries are clean.
Models
Gemini 3.8 Flash is used for indexing and Voyage 3.5 is used to generate embeddings.
Summaries: language and secrets
- Language. Summaries are written in the conversation's main language. When a conversation mixes languages (e.g. English logs in a Spanish chat), the user's language wins; code, identifiers and quoted text stay as written.
- Secrets. Passwords, API keys, tokens and private keys we detect are replaced with
[REDACTED:<kind>]in the text we index, so summaries don't contain them, and in every MemTree view (page,.json, search, lines, messages, shared and partner views).
Usage
API Keys
Click here to generate and copy your PolyChat API key. Your API key can be further managed from within the polychat.co app under Settings > Account > API keys.
Claude Code: Claude Code Infinite
Claude Code Infinite (ccc) runs Claude Code with Context Memory: a small local proxy sends each conversation through /v1/context_memory, so sessions run past the context window without compacting, and its MemTree tools let the agent search and read earlier parts of the session. Your Anthropic login stays on your machine. Install it (Node 18 or newer) and run ccc:
npm install -g https://github.com/crizCraig/claude-code-infinite/tarball/main
ccc
Setup Context Memory with Kilo Code, Roo Code, or Cline
- Set the Base URL to
https://polychat.co/api - Get your API key from within the polychat.co app under Settings > Account > API keys
- Add the Custom Header:
- name:
x-polychat-memory - value:
on
- name:
/context_memory
This API takes in OpenAI compatible messages and outputs compressed OpenAI messages. It allows you to easily add memory to any model by replacing the original messages with the compressed messages this API returns.
Send a POST request with messages, your api key, and the context limit to compress messages. model and tools are optional but recommended: they size the budget and count the tools against it.
curl -X POST https://api.polychat.co/v1/context_memory \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"}
],
"model_context_limit": 200000,
"model": "claude-sonnet-4-5-20250929",
"tools": [{"name": "search", "input_schema": {"type": "object"}}]
}'
https://polychat.co/api/context_memory also accepts the same request, but it does not pass back the X-Polychat-Memtree-Url header or forward x-client headers; prefer https://api.polychat.co/v1/context_memory.
This outputs compressed messages, a verdict, usage, and the model's budget:
{
"messages": [
{
"role": "user",
"content": "[Memory message will be here*]" // Present once an index exists and the request is over budget
},
{
"role": "assistant",
"content": "…recent messages, verbatim…"
}
],
"compressed": true, // false => under budget (or not indexed yet); forward your original messages
"usage": {
"prompt_tokens": 260000,
"prompt_tokens_details": {"cached_tokens": 248000},
"completion_tokens": 47000,
"total_tokens": 307000,
"raw_prompt_tokens": 266000,
"indexed_tokens": 248000
},
"model_budget_tokens": 50000,
"index_pending": false,
"memory_status": "ok"
}
Params
- messages (required): OpenAI compatible messages to compress. This is a list of messages where each message is a dictionary with
roleandcontentkeys. Each message can contain nesting according to the OpenAI API standard. Anthropic-style content blocks (tool_use,tool_result, images, thinking) are accepted too. - model_context_limit (required): The real context window, in tokens, of the model you will send the compressed messages to (see indexing). Minimum 10,000. This is the hard cap: a conversation that cannot fit in it before its memory is built gets a 413. To control how small conversations are compressed, use
compression_target_tokensrather than lowering this value. - model (optional): The model id you will send the compressed messages to, e.g.
claude-opus-5,claude-sonnet-4-5-20250929,anthropic/claude-fable-5-1[1m]. Selects the model-based budget. Omitted or unrecognized models get the 50k-token default. - tools (optional): The tool definitions you will send with the request (OpenAI or Anthropic format). Their serialized size counts against the budget, since they occupy the same context as the messages. They are not modified or returned.
- compression_target_tokens (optional): Whole-request soft target in tokens (system prompt + tools + memory + recent messages) that compression aims at. Overrides the model-based policy. Either
-1to never compress — every call returns the conversation uncompressed with"compressed": falsewhile indexing continues in the background — or an integer of at least 10,000 (a positive value below that is a 400;0is a validation error). Clamped tomodel_context_limit. Omit it to use the model policy.budget_tokensis accepted as a deprecated alias. - compression_threshold_tokens (optional): Whole-request size above which compression is attempted. Minimum 10,000; capped at
model_context_limit. At or below this threshold, the request passes through. When omitted, the effective compression target is also the threshold. Compression still aims atcompression_target_tokens, so you can set a higher threshold and a lower target. - client_input_tokens (optional): A nonnegative integer giving your measured or calibrated estimate of the complete request's input tokens, including the system prompt and tools. Omitted or
nullkeeps the server's character-based decision. A value strictly above the effective threshold rules out the under-budget passthrough even when the server's estimate fits. A smaller value cannot prevent compression required by the server's estimate.compression_target_tokens: -1still disables compression, andindex_onlyignores this hint. It does not change the target, hard window, minimum target, or normal billing; the hint itself is never a billed token count. Without a ready index, a request fitting the server's hard-window check still returns"compressed": false; otherwise the usual indexing wait and 413 behavior applies. Clients must inspectcompressedand check whether the original request fits their provider's window before forwarding it. - flatten (optional, default
false): Whentrueand the response is compressed, the response additionally carriesflattened_messages: the compressed conversation collapsed into a single user message (system messages excluded, since you keep your own system prompt). Useful for Anthropic-style clients that must preserve tool-use adjacency. Never present on a passthrough ("compressed": false). - index_only (optional, default
false): Trigger indexing for these messages and return immediately with{"messages": [], "usage": {…}, "index_only": true}. No compression and no output;usage.prompt_tokensis the un-indexed tail this call handed to an indexing job, billed at the input rate (often0; see pricing). Intended for frequent tool-turn calls that only need to keep the index warm. - final_index (optional, default
false; only withindex_only): The conversation has ended, so index its un-indexed tail even when it is below the 10,000-token minimum that normally starts a job. Honored at most once per conversation every 30 minutes; otherwise the call behaves like a plainindex_only. The tail is billed like any index-only tail. Claude Code Infinite sends it after 10 minutes idle following Stop, or at exit for at most the three newest eligible sessions, within a shared 2-second preparation-and-upload deadline. Short conversations are eligible too. Conversations with an identical opening message share the 30-minute allowance. Older servers ignore it. - expiration_days (optional): An integer from
0to3650. A positive value is the retention in days: memory left unused by Context Memory calls for longer than that expires.0or omitted means it never expires; there is no default retention, so send yours on every request. A value outside the range is a 422. Expired memory is never reused (a later request builds a new index); on partner keys it also can no longer be opened (see retention).
Response fields
- messages: The messages to send to your model. Compressed (memory message + recent messages) when
compressedistrue; your conversation, sanitized, when it isfalse. - compressed: Whether compression actually happened.
falsemeans the request was under the budget, the index is not ready yet, orcompression_target_tokenswas-1. Gate passthrough on this field, not onusage, which reports what was billed. - flattened_messages: Only with
flatten: trueandcompressed: true(see above). - usage: the billed quantities, then two informational fields (see pricing):
prompt_tokens: billed input tokens: the whole conversation's billable input. The exception is a Claude Code Infinite passthrough, which bills only the new tokens this call handed to an indexing job (often0; see pricing).prompt_tokens_details.cached_tokens: the part ofprompt_tokensbilled at the cached rate.0on a Claude Code Infinite passthrough.completion_tokens: tokens in the returned conversation (memory plus recent messages, or your conversation unchanged on a passthrough).0on a Claude Code Infinite passthrough.total_tokens:prompt_tokens+completion_tokens.raw_prompt_tokens: a conservative estimate of the input as you sent it, including the system prompt and an estimate for images. For before/after comparisons only; not billed.indexed_tokens: how much of the billable input needs no new indexing: what the index already covers, or all of it when this call starts no indexing job (for example while fewer than 10,000 tokens are un-indexed). Equal tocached_tokens, except on a Claude Code Infinite passthrough, where nothing bills as cached. Not billed. Absent onindex_onlyresponses.
- model_budget_tokens: the whole-request budget of the
modelyou sent, in tokens, capped atmodel_context_limit. It ignorescompression_target_tokens, so a client that sets its own target still learns the model's budget, and it is present on passthroughs too: compare the size of what you send against it to decide when compression will next apply. Informational; not billed. Absent onindex_onlyresponses. - index_pending:
truewhen the request has messages beyond the index this response was built from and an indexing job that will cover them is queued or running (wait for it if you need them indexed);falsewhen the index is current or this request found no live job that will cover them (e.g. a tail below the indexing threshold, even if a final index of it is already running, or indexing failed). Also onindex_onlyresponses. - memory_status: whether all of this conversation could be added to memory. Also on
index_onlyresponses, and in theX-Polychat-Memory-Statusheader. One of:ok: nothing was left out.partial_content_blocked: part of this chat couldn't be added to memory because it was blocked by our model provider's content filter. Those messages are not in the memory, so a compressed response does not include them; the rest of the chat is remembered as usual.content_blocked: this chat couldn't be added to memory because it was blocked by our model provider's content filter. The response is a passthrough ("compressed": false). A chat gets this while it has no memory at all because of a block (later parts may still be indexed; seeindex_pending), and for good once the filter has blocked three separate parts of it: from then on nothing more is indexed.
ok.
Headers
- Authorization:
Bearer <API key>(required). - x-client / x-client-version (optional): Identify your integration (e.g.
cc-infinite/1.0.12). Lets us reach you about breaking changes and gate version-specific fixes. - x-client-meta (optional; partners): a JSON object. Partners send
{"end_user_id": "u_6f1c2d3e"}to record which of their users a request belongs to (see partners). Unknown keys and malformed values are dropped, never an error.
Response headers:
- X-Polychat-Memtree-Url (optional): this request's MemTree page (see finding the tree). Absent on
index_onlyresponses and errors, including 413s. For partner keys, present only when a tree is ready to view (absent, for example, until a new conversation's first tree is built, or without anend_user_id). - X-Polychat-Memory-Status: the response's
memory_status(ok,partial_content_blockedorcontent_blocked). On compressed, passthrough andindex_onlyresponses; absent on errors. - X-Polychat-Usage-Url: internal; ignore it.
GET /v1/context_memory/status
At https://api.polychat.co/v1/context_memory/status, with the same auth as the POST. Returns {"email": …, "paid": true|false, "payment_message": …} so a client can find out at startup whether compression calls would be rejected with 402, without sending a conversation.
Errors
- 400:
model_context_limit, a positivecompression_target_tokens, orcompression_threshold_tokensbelow 10,000. - 422: A malformed request body, e.g. a missing required field,
compression_target_tokens: 0, orexpiration_daysoutside 0–3650. - 401: Missing or invalid API key.
- 402: Payment required; the
detailcarries the message shown by/status. Applies toindex_onlycalls too. - 413: The whole request (messages, system prompt and tools) still exceeds
model_context_limitafter compression. While an indexing job is running we wait for it (up to about 5 minutes) first (see indexing).detailcarries an estimated indexing time. A 413 is not billed and has noX-Polychat-Memtree-Url. If indexing is still pending, retry after the estimate; a request that still exceeds the window once indexing is done (e.g. a most recent message larger than it) will 413 every time.
/chat_completions
This is a fully OpenAI compatible API which uses models available on polychat.co to generate responses. It uses Context Memory under the hood to compress the messages for you. Send it uncompressed messages, and we will compress them and send them to the model.
Send a POST request with messages, your api key, and the x-polychat-memory header set to "on"
curl -X POST https://polychat.co/api/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "x-polychat-memory: on" \
-d '{
"model": "GPT-4o-mini",
"messages": [
{
"role": "user",
"content": "hi"
}
]
}'
System Prompt
We don't summarize the system prompt: it passes through and does not get reflected in the memory index. The one exception is size: a system prompt longer than 60,000 characters may be trimmed in the returned messages to its first 3,000 and last 57,000 characters, with a truncation marker between them. We do this as system prompts are typically important for every message turn (agent instructions, tools available, provider info, etc...). And since they can contain today's date/time, they can invalidate the memory.
Partners: your users' memory
Partners (currently NanoGPT) send many users' conversations under one partner key. The endpoints below let you show one of your users the memory behind a response, and delete it. They take your partner key (Authorization: Bearer <partner key>), are called from your backend only, and are enabled per partner: until yours is, they answer 404 like an unknown route. Base URL: https://api.polychat.co (app.polychat.co, the host in X-Polychat-Memtree-Url, serves the same endpoints).
Send the end user on every call
On each /v1/context_memory call, name the end user in x-client-meta:
curl -X POST https://api.polychat.co/v1/context_memory \
-H "Authorization: Bearer $PARTNER_KEY" \
-H 'x-client-meta: {"end_user_id": "u_6f1c2d3e"}' \
-H "Content-Type: application/json" \
-d '{"messages": [...], "model_context_limit": 200000, "expiration_days": 30}'
end_user_id: 1–128 printable ASCII characters, no spaces. Use a stable, opaque id (random, or a keyed hash), never an email or your internal account id. One id per chat is fine.- Required for viewing. A request sent without it is never viewable: it cannot get a view code, now or later.
- Isolation. Memory from one end user is never shown to another: a page opens only for the end user its request was recorded with, and a conversation builds on another conversation's memory only when that memory belongs to the same end user or covers identical messages.
- Store the
X-Polychat-Memtree-Urlpage id with the message it came from; you will pass it to the calls below. The header is present only when a tree is ready to view, so show “View memory” exactly on the messages that have one.
Retention
expiration_days is stored with each request, and each tree keeps the retention of the request that created it. A positive value is the retention in days, counted from the tree's last use by a Context Memory call (viewing does not extend it). 0 or omitted means the memory never expires; there is no default, so if your users have one, send it on every request. Once expired, memory can no longer be opened (view-code answers 410, open pages answer “This memory has expired”), listed or reused, and it is then deleted.
POST /partner/memtree/{id}/view-code
When your user clicks “view memory”, check on your side that the message is theirs, then mint a one-time code. {id} is the page id from X-Polychat-Memtree-Url, exactly as sent (-s included), or the full request id. end_user_id is required and must be the id the request was recorded with.
curl -X POST https://api.polychat.co/partner/memtree/2g36dGG1een5-s/view-code \
-H "Authorization: Bearer $PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"end_user_id": "u_6f1c2d3e"}'
{
"request_id": "0f1c2d3e-4a5b-4c7d-8e9f-0a1b2c3d4e5f",
"code": "Qk3…",
"expires_in": 60,
"expires_at": "2026-10-14T18:46:06+00:00",
"url": "https://app.polychat.co/m/2g36dGG1een5-s?code=Qk3…",
"redeem_url": "https://app.polychat.co/m/2g36dGG1een5-s/redeem"
}
- Scope. The code can be redeemed once, within 60 seconds, and opens that one request's MemTree page (its own tree, or the tree it was served from with
-s), nothing else. Mint a new one every time you open the viewer. A code can be minted while the tree is still building; the page then shows that it is building. - 400: the body is not a JSON object, or
end_user_idis missing or malformed. - 401: missing or wrong partner key.
- 404: no such request under your key, a request with no memory, a request recorded for another
end_user_idor none, or a tree we will not show to that user. All get the same answer, so nothing says which. - 410: the tree is past its retention; nothing is minted.
Opening the page
Preferred: form-POST the code to redeem_url from the user's browser, so the code never appears in a URL:
<form id="f" method="post" action="{redeem_url}">
<input type="hidden" name="code" value="{code}">
</form>
<script>document.getElementById("f").submit()</script>
Or open url with a GET (a new tab, a redirect, or an iframe src on your allowed origins). The code is spent on first use and the page removes it from the address bar. A used, expired or unknown code gets one generic “This link is invalid or has expired” page (404).
The page shows that one tree and the conversation behind it. It does not show your system prompts, developer messages or tool definitions, has no account, billing or usage details and no links to other trees, and masks secrets. A reload works for a while in the same browser; a copied link without a code shows nothing.
DELETE /partner/memtree
Call it when a user deletes a chat or their account. It only touches requests sent with your key.
curl -X DELETE "https://api.polychat.co/partner/memtree?end_user_id=u_6f1c2d3e" \
-H "Authorization: Bearer $PARTNER_KEY"
curl -X DELETE https://api.polychat.co/partner/memtree/2g36dGG1een5-s \
-H "Authorization: Bearer $PARTNER_KEY"
?end_user_id=…: every request recorded with that id and every tree of its conversation, including all branches and the earlier trees it built on. With a per-user id, that is all of the user's memory.end_user_idmissing or malformed is a 400./{id}(page id or request id): that request's tree and stored copies. A tree still used by the same conversation's later trees is kept; delete byend_user_idto remove a whole chat. An unknown id is a 404. A response withoutX-Polychat-Memtree-Urlhas no page id: delete its request byend_user_id.- What goes: the trees and their summaries, search embeddings, the stored conversation and our stored copies of the requests. Billing records (token counts, no content) stay.
- What stays: memory is stored by content, so conversations that start with identical messages share their early trees. A tree another conversation still builds on is kept (counted in
kept_shared) until that conversation is deleted or expires. - Idempotent: repeating a completed delete returns zero counts (while
cleanup_pendingis nonzero, a repeat can count that cleanup again). There is no multi-conversation guard: the olderdelete_more_than_oneflag is still accepted (true/false) and changes nothing.
{
"end_user_id": "u_6f1c2d3e",
"conversations": 1,
"trees_deleted": 7,
"kept_shared": 1,
"in_progress": 0,
"embeddings_deleted": 412,
"stored_objects_deleted": 950,
"requests_unlinked": 9,
"shares_revoked": 0,
"cleanup_pending": 0
}
DELETE /partner/memtree/{id} returns request_id instead of end_user_id. Deleted trees and the requests' links to them are unreachable once the call returns; trees in kept_shared stay for the conversations that still use them. in_progress counts trees still being built, which are deleted once they finish (unless something else still uses them); cleanup_pending counts storage cleanup that will be retried (the data is already unreachable).
Pricing
Context Memory is billed per token at these rates, with no markup on top:
| Token Type | Cost per million tokens |
|---|---|
| Input | $3.75 |
| Cached input | $1.00 |
| Output | $1.25 |
What each call bills (the same quantities usage reports):
- Compressed (
"compressed": true): input the index already covers at the cached rate, any new input handed to an indexing job at the input rate, and the returned conversation (memory plus recent messages) as output. For long conversations most input is cached, and output is sized by the whole-request budget for your model, which you can set per request withcompression_target_tokens. - Passthrough (
"compressed": false), billed by caller:- Claude Code Infinite (requests sent with
x-client: cc-infiniteand your own PolyChat API key): only the new tokens this call hands to an indexing job, at the input rate. Input the index already covers is free, and so is the output, which is your conversation unchanged (cached_tokensandcompletion_tokensare0). A passthrough that starts no indexing job, for example while fewer than 10,000 tokens are un-indexed, bills nothing. - All other callers (other clients, partner keys, and requests through polychat.co): billed like a compressed call: input the index already covers at the cached rate, any new input handed to an indexing job at the input rate, and the returned conversation (your conversation unchanged) as output.
- Claude Code Infinite (requests sent with
- Index-only (
index_only: true): the un-indexed tail the call hands to an indexing job, at the input rate; no cached or output tokens. Input already covered, or already handed to a job by an earlier call, is not billed again. - Content blocked by our model provider's filter (see
memory_status): content the filter blocks isn't added to memory. Calls are billed the same way whatever theirmemory_status: acontent_blockedcall is billed like any passthrough. Use the status to decide whether to keep using Memory for that chat.
The system message, Claude Code's <system-reminder> blocks, images and other media, and tool definitions are not billed. Calls that search the memory also carry a small memory-search charge for embedding the query, and a vector search across sessions bills its query embedding (see search). Reading a MemTree is free.