Look at the next tool result your agent gets back from an API. Something like this:
[{"id":"A-1","status":"open","assignee":"ann","labels":["bug"]},
{"id":"A-2","status":"closed","assignee":"bob","labels":[]},
...]
Now imagine 100 rows. The words id, status, assignee and labels are sent 100 times each, with quotes around every one. The model doesn't need them 100 times. It needs them once.
That's the whole idea behind LWON (Lightweight Object Notation): JSON with the keys written once. A header names the fields; the body holds only the values, by position.
What it looks like
{[orders[id customer[name city] [items[sku qty price]]]] page}
[[
[A-1 [Ann Istanbul] [[SKU1 2 9.99] [SKU2 1 19.5]]]
[A-2 [Bob Izmir] []]
] 1]
That is exactly this JSON:
{"orders":[{"id":"A-1","customer":{"name":"Ann","city":"Istanbul"},"items":[{"sku":"SKU1","qty":2,"price":9.99},{"sku":"SKU2","qty":1,"price":19.5}]},{"id":"A-2","customer":{"name":"Bob","city":"Izmir"},"items":[]}],"page":1}
Line 1 is the shape. Every row after it is just values. Nested objects and arrays of objects get their own fields in the header, so it stays compact even when the data isn't a flat table.
The hard requirement I set for myself: any JSON value must convert to LWON and back without loss. Escapes, null versus a missing key, objects with different fields, weird keys, primitive roots. The test suite runs a language-neutral conformance set plus 3000 fuzzed round trips.
"Isn't this just TOON / CSV?"
Fair question. TOON has the same goal and is very good on flat tables. CSV is fine until your data has nesting.
The difference shows up on the data agents actually see: API responses with nested objects, trees, and optional fields. Here are Claude token counts (via the count_tokens API) on six datasets:
| Dataset | JSON (minified) | TOON | LWON |
|---|---|---|---|
| Flat table, 100 rows | 4295 | 2510 | 2503 |
| Nested orders | 8259 | 7435 | 4808 |
| GitHub-style issues | 4378 | 4414 | 2760 |
| Recursive menu | 562 | 578 | 362 |
| Logs with optional fields | 4393 | 4472 | 3452 |
| Search tool result | 2930 | 2200 | 2298 |
On flat tables LWON and TOON are basically tied, and TOON wins the search result. On nested and recursive data, LWON is 21–42% smaller than minified JSON, while table formats fall back to layouts close to JSON's size.
Fewer tokens is worthless if the model gets it wrong
This was the part I cared about most. A format that saves 40% and makes the model misread a field is a net loss.
So I wrote an eval: 44 questions about the benchmark data (lookups, counts, nested fields, trees, absent versus null), asked in JSON, TOON and LWON, to Claude Haiku, Sonnet and Opus.
Every model got every question right in all three formats.
Then I made it harder: 150 sparse log events where most fields are missing in most rows. Here LWON missed 11 of 176 answers and JSON missed 9. All misses in both formats were counts over many rows off by one to three, or a sum.
I want to be upfront about that: on very sparse data, LWON is slightly behind JSON on accuracy, not ahead.
The thing I got wrong in 0.x
Input tokens are not the whole bill. Models think about the data, and output tokens cost more than input.
In LWON 0.x, every key was written by position, so sparse rows looked like [x ~ ~ ~ ~ ~ 3 ~ ~ ~]. The models had to count positions through all those ~ placeholders, reasoned much longer, and on the sparse data a question cost up to 3× as much as with JSON. Smaller input, bigger bill.
The fix in 1.0: rare keys are written by name in a trailing * slot. Common fields stay positional, rare ones stay readable.
Counting everything (input, thinking, output), a question now costs:
- 21–25% less than JSON on regular data sent once (2–15% less when cached)
- 9–11% less on sparse data sent once, but 6–9% more when cached
So: measure on your data. That's why the repo ships the eval scripts, not just the numbers.
Try it on your own JSON
The quickest way to see if LWON helps you is the playground. Paste any JSON, or load one of the benchmark datasets, and it converts both ways and shows the token count next to minified JSON.
Everything runs in your browser; your data isn't sent anywhere. Tokens there are counted with the o200k tokenizer, so the exact numbers differ a little from Claude's, but the ratio is close.
Using it
npm install @lwon/core
import { encode, decode, PRIMER } from '@lwon/core';
const text = encode(data); // any JSON value → LWON
const back = decode(text); // LWON → the same JSON value
// Models don't know LWON yet, so send a short primer (~330 tokens) once.
const prompt = `${PRIMER}\n\n${text}`;
One rule: convert before the data reaches the model, in your tool or API layer. Asking the model to convert saves nothing.
There's also a CLI if you want to check your own data first:
npx @lwon/core stats data.json # JSON vs LWON size
Zero-code: an MCP proxy
Most of the JSON my agents see comes from MCP tool results, so I wrote @lwon/mcp-proxy. Put it in front of any stdio MCP server and large JSON tool results reach the model as LWON. The server doesn't change.
claude mcp add github -- npx -y @lwon/mcp-proxy -- npx -y @modelcontextprotocol/server-github
It only rewrites a result when it's JSON, at least 2000 characters, and at least 10% shorter as LWON. Errors, plain text, images and structuredContent pass through untouched. The primer goes into the server's instructions once per session, not once per result.
Status and what I'd love feedback on
LWON is 1.0 beta: the spec, a dependency-free TypeScript library, a conformance suite, and the MCP proxy, under Apache-2.0. Python and Go ports are next.
I'd really like to hear:
- Your data. Paste a real tool result into the playground and tell me the number, especially if LWON loses.
- Other models. My evals are Claude-only so far. If you test GPT or Gemini, I'll add the results.
The spec. If you find a JSON value that doesn't round-trip, that's a bug I want.
Playground: https://ahmetzencirli.github.io/lwon-studio/playground
Website and spec: https://ahmetzencirli.github.io/lwon-studio/
Thanks for reading!
Top comments (7)
dropping duplicate JSON keys to save token bloat is proper clever. as the founder and CEO of AGI Systems Directorate, we deal with massive structured payloads and token streams for our brain-inspired agent architectures and neural-symbolic models, so cutting down redundant structural overhead hits home.
tested out the playground over on your github.io pages with a heavier multi-layered agent state payload. LWON uses 997 tokens, −13% compared with 1,151 for minified JSON.
also, seeing as you're hosting on a github.io subdomain, getting Google Search Console indexing and crawler visibility properly mapped out can be a right pain. dropped some handy configs for that in our open repos.
your post literally reminded me of the Python library we're building for benchmarking AI agents. posted about our open contributor challenge for it here yesterday as well.
Like a work in artificial intelligence. And artificial general intelligence so just want to know what are you working on. And I like to connect with other developers working in the field
Thanks for trying it on a real payload, that's exactly the kind of number I was hoping people would post.
13% is in line with what I'd expect for agent state. LWON's savings come from keys that repeat: arrays of objects with the same fields. A single deep state object usually has each key only once, plus long string values (messages, prompts, tool output), so there's little repetition to remove and you mostly save quotes and punctuation. If your payload has a list inside it (message history, tool calls, steps), that part is where the savings will be.
One note: the playground counts tokens with the o200k tokenizer, so Claude's numbers will be a bit different, but the ratio is usually close.
If you can share the rough shape of the payload (keys only, no data), I'd be happy to look at whether the encoder could do better on it.
Okay I will definitely tell you about it. But after sometime. Because I am busy in founder and CEO work. But I will definitely tell you
@ahmet_zencirli Stale context is compacted with the prompt cache in mind, and each fold keeps a hash, so
tokenade expand-refreturns the exact original bytes. Your cached numbers are a fair correction: the THOL figure is session cost in USD, end to end, so cache reads are counted at their price.Your "fewer tokens is worthless if the model gets it wrong" section is the right instinct,
but the eval as designed can't see a difference: 44 questions, three formats, every model
perfect. That's a ceiling, not a result. If LWON silently loses a nested field, nothing in
that set fails — none of the questions force the model to read a value by position.
The sparse set is closer, but 11 misses vs 9 over 176 answers is a two-answer gap. That's
inside noise for one run. I'd report it as "no measurable difference" rather than "slightly
behind JSON" — the honest version is also the stronger one.
What would actually discriminate: (a) absent-vs-null at depth, scored per question type
instead of as a total, since positional layouts break exactly there; (b) tool-call
arguments in LWON. You measured comprehension, not generation. Reading the header is
cheap; emitting [[A-1 [Ann Istanbul] [[SKU1 2 9.99]]]] correctly on the first attempt is
where I'd expect the failures, and that's the one that costs a round trip plus a retry.
One number in your favour, from a 10,000-prompt measurement of chars/4 against cl100k:
structured/JSON is the only content group where the estimator under-counts (median -5.6%,
21% of rows off by more than 20% low, against 3% high). JSON is token-dense per character,
not sparse — that's the mechanism behind your 21-42% on nested data, and it means the
dollar saving is larger than any chars/4-based estimate would show. Which is also why you
shouldn't validate the swap with chars/4: its error flips sign by content type, so a
chars/4 A/B measures the estimator, not the format.
Building Tokenade showed me the history weighs more than the payload: earlier tool results are sent once more with each later call. Shrinking one result helps once. Shrinking what stays in the history helps for the rest of the session.
Agreed, and that's the main reason the MCP proxy converts at the source rather than somewhere later in the pipeline. The result enters the history as LWON, so the savings repeat on every later call in the session instead of once.
Two caveats from my own measurements. With prompt caching, most of that history is read from the cache at a fraction of the input price, so the cost multiplier is smaller than the raw token count suggests. In my evals the savings dropped from 21–25% to 2–15% once the data was cached, and on very sparse data LWON was actually 6–9% more expensive cached, because the model spent more output tokens reading it. The context window is where the multiplier holds fully: every token that stays in the history keeps taking up room and brings compaction closer.
What does Tokenade do with the history? Trim old results, summarize them, or something else? Curious how it compares.