A decision model in a browser tab
Type a Bluesky handle into the Account Decider and, a moment later, a model running on your own computer's graphics chip tells you what kind of account it is: a person, a bot, an organization or a news feed. It also says how much the account posts about AI, in what capacity (researcher, builder, commentator, artist, marketer), with what attitude, and which of its recent posts are about AI. The model is a 10 MB file. Nothing leaves the page except the requests to Bluesky's public API that fetch the account's posts.
The model learned this by imitating Claude Sonnet. I had Sonnet read 1,842 Bluesky accounts and answer the same questions for each, for $14.58 of API time, and then trained a model with 17 million parameters (the adjustable numbers inside it; the largest open chatbots have hundreds of billions) to give the answers Sonnet gave. On 300 accounts it never saw in training, it picks Sonnet's answer 81 to 94 percent of the time, depending on the question. It only knows these questions: shown a new set of questions, the same kind of model does worse than guessing.
A decision model answers typed questions with probabilities
Most AI that people use writes text. A decision model does not. You give it a piece of state (a support ticket, an invoice, an agent's log, a social media profile) and a few typed questions, and it returns a probability for every possible answer: 71 percent "refund", 20 percent "escalate", 9 percent "close". The questions come in three shapes. A choice picks one label from a list. A score places the state on a rubric, such as urgency from 0 to 3. A yes/no question gives the probability that a statement is true.
TypeSafe's Jev is a hosted one, and Oskar asked whether something like it could run in a web page. Jev answers questions it has never seen before (in the field's word, zero-shot) in under a second, and it bills by input text: a request the size of this paragraph costs a few thousandths of a cent. Muninn uses it to route and triage its own work. Because the answer is a set of numbers rather than prose, a program can act on it directly, and the probabilities say how sure the model is. A hosted model also means a network round trip for every call, and the data has to leave the user's machine.
A 17M-parameter model matches Jev on a public benchmark
The test bed is Typed Decisions, a public benchmark of four synthetic business workflows: customer-service threads, vendor invoices, security alerts and AI-agent run logs. Each case comes with five questions. The reference answers are the average of three judgments by a teacher model, so a score measures agreement with that teacher. The benchmark's leaderboard lists Jev, zero-shot, at 0.727: it picks the teacher's answer 72.7 percent of the time without having seen these workflows. One fresh sample from the teacher agrees with the averaged answer 0.735 of the time. The dataset card notes that a model trained on the benchmark can score well above that, by learning the teacher's habits.
The small model is Ettin-17m, an open text encoder from Johns Hopkins. An encoder reads a whole passage at once and produces a vector, a list of numbers, for every word in it; a chatbot model instead writes text one word at a time. Its published weights are a 68 MB file. An earlier experiment had already shrunk it to 10 MB, a 6.6-fold cut, by storing the weights in 8 bits and the vocabulary table in 4, for Bluesky Answer Search, a search engine for Bluesky posts that runs in the browser. To make it a decision model, I wrote each question out as one passage, with a placeholder slot in front of every option, followed by the state:
Data
| option | example probability |
|---|---|
| human | 93% |
| bot | 3% |
| org | 3% |
| feed | 1% |
Fine-tuning, the training step, adjusts the whole model so that those probabilities match the teacher's. Ten passes over the benchmark's 1,200 training cases took about four minutes on a rented cloud GPU.
Trained on the four workflows, Ettin-17m scores 0.740, averaged over three training runs: Jev's zero-shot number, on questions Ettin was trained for and Jev was not. Pleias' Monad, a 56M-parameter language model that already runs on another page here, scores 0.752 the same way. That page runs an 8-bit, 65 MB version whose weights I edited to stop it repeating itself. The decision model started from Pleias' original weights instead, because it writes no text that could loop, and I scored it at full precision without building a browser file. The browser file stores each layer weight as one of 63 levels and each vocabulary entry in 4 bits, instead of 32-bit numbers. Over three retrained copies of the model that costs nothing measurable (0.732 against 0.7315 at full precision), and the file on the page, retrained on 6 November, scores 0.745.
The leaderboard has a second table for models fine-tuned on the benchmark, as these two were. Its scores are mostly reported by the models' own authors; my two were measured here. Plotted by parameter count, Ettin-17m is the smallest model in the table at its accuracy whose behaviour was all learned from data. Larger models do better, up to 0.826 for a 4-billion-parameter one. One entry beats it with a third of the parameters: nanoGentzen pairs a 4.9M-parameter model with rules written by hand for these four workflows, so part of what it knows was typed in rather than trained.
this experiment leaderboard entries best accuracy at that size or smaller
Data
| model | parameters | accuracy |
|---|---|---|
| Ettin-17m, fine-tuned (this experiment) | 17M | 0.740 |
| Monad 56M, fine-tuned (this experiment) | 56M | 0.752 |
| nanoGentzen-v3 | 4.9M | 0.7585 |
| Bekko 17M | 17M | 0.4830 |
| MiniLM-L6 (card's own) | 22M | 0.5870 |
| Bekko 68M | 68M | 0.5370 |
| SafeDecide-140M | 140M | 0.7540 |
| Phocinae-Largha-150M (mmBERT-small) | 144M | 0.7910 |
| tasksource-decider-nano | 149M | 0.7515 |
| ModernBERT-base (card's own) | 149M | 0.6460 |
| Rev-gen1-UltraBERT-150M | 150M | 0.7630 |
| gemma3-270m | 270M | 0.7610 |
| gemma3-270m (general) | 270M | 0.7020 |
| Rev-gen1-EmbeddingGemma-2-Frozen | 272M | 0.6015 |
| embeddinggemma-2-ft | 300M | 0.7700 |
| Rev-gen1-UltraBERT-380M | 378M | 0.7620 |
| nullspan (ModernBERT-large) | 395M | 0.7900 |
| sysone-0.4B (Ettin-400M) | 400M | 0.8025 |
| OpenDecider-nano | 400M | 0.7960 |
| SafeDecide-400M | 400M | 0.7870 |
| Bekko 400M | 400M | 0.6680 |
| soft-decider-421m | 421M | 0.7740 |
| ProKope-421M | 421M | 0.7185 |
| Rev-gen1-nano | 752M | 0.7515 |
| pramana-0.8b | 800M | 0.7970 |
| lazydecider-0.8b | 800M | 0.7700 |
| serin-0.8b | 800M | 0.7165 |
| vega-08b | 800M | 0.6630 |
| Rev-gen1-micro-lite | 1,300M | 0.7645 |
| Rev-gen1-micro | 1,900M | 0.7645 |
| pramana-2b | 2,000M | 0.8005 |
| Rev-gen1-mini-lite | 2,800M | 0.7775 |
| ministral3-3b | 3,000M | 0.8080 |
| Decider-4B | 4,000M | 0.8260 |
| Arbiter-4B | 4,000M | 0.8170 |
| od1 | 4,000M | 0.7970 |
| OpenDecider-small-td | 4,000M | 0.7920 |
| Qwen3.5-4B-typed-decisions | 4,000M | 0.7725 |
| Rev-gen1-mini | 4,200M | 0.7760 |
| Seb-9B | 9,000M | 0.7540 |
| FastBrain-9B (priors fitted) | 9,400M | 0.7970 |
| Rev-gen1-midi-lite | 15,700M | 0.7885 |
| Rev-gen1-midi | 25,600M | 0.7945 |
| Darwin-27B-ZTC-v5 | 27,000M | 0.8120 |
| Amos 1 large (priors fitted) | 27,000M | 0.8025 |
| OpenDecider-medium-td | 30,000M | 0.7880 |
| OpenDecider-large-td | 80,000M | 0.8010 |
A browser downloads bytes, not parameters. nanoGentzen's author publishes its weights at 32 bits per number, so its file is 19.6 MB, about twice the 9.7 MB Ettin file. Most of the larger entries publish 16- or 32-bit files. Plotted by the smallest file each author publishes, Ettin-17m is the smallest download on the chart. Two models under 100 MB score higher: nanoGentzen, about 0.01 more for twice the bytes, and Monad, whose 8-bit file would be about 65 MB. nanoGentzen's weights could be quantized the same way; nobody has published that file.
this experiment leaderboard entries best accuracy at that size or smaller
Data
| model | file size | accuracy | file |
|---|---|---|---|
| Ettin-17m, fine-tuned (this experiment) | 9.7 MB | 0.745 | browser file, 63-level layer weights and 4-bit vocabulary table; score measured on that file |
| Monad 56M, fine-tuned (this experiment) | 65.0 MB | 0.752 | estimate: size of the chat page's 8-bit Monad; decision model not packed; score at full precision |
| nanoGentzen-v3 | 19.6 MB | 0.7585 | model.safetensors, 32-bit; hand-written rules not counted |
| Bekko 17M | 29.0 MB | 0.483 | onnx_browser/model.onnx |
| Bekko 68M | 196.3 MB | 0.537 | onnx_browser/model.onnx |
| Bekko 400M | 1,426.2 MB | 0.668 | onnx_browser/model.onnx |
| SafeDecide-140M | 141.6 MB | 0.754 | onnx/model_quantized.onnx, 8-bit |
| tasksource-decider-nano | 298.0 MB | 0.7515 | model.safetensors, 16-bit |
| Phocinae-Largha-150M | 288.6 MB | 0.791 | model.safetensors, 16-bit |
| nullspan (ModernBERT-large) | 347.2 MB | 0.79 | onnx/model_quantized.onnx, 8-bit |
| sysone-0.4B (Ettin-400M) | 715.8 MB | 0.8025 | backbone + head safetensors |
| OpenDecider-nano | 791.7 MB | 0.796 | model + head safetensors, 16-bit |
| soft-decider-421m | 842.6 MB | 0.774 | model.safetensors, 16-bit |
| ProKope-421M | 842.6 MB | 0.7185 | model.safetensors, 16-bit |
| gemma3-270m | 1,072.4 MB | 0.761 | model.safetensors, 32-bit |
| gemma3-270m (general) | 1,072.4 MB | 0.702 | model.safetensors, 32-bit |
| embeddinggemma-2-ft | 1,088.5 MB | 0.77 | model.safetensors, 32-bit |
| SafeDecide-400M | 1,583.3 MB | 0.787 | model.safetensors, 32-bit |
nanoGentzen's author also published a separate release, nanoGentzen-Jev-v3, whose model card breaks its score down by question type. Its card reports 0.731 overall, below the leaderboard entry. Ettin-17m on the same 400 test cases and 2,000 decisions, with the benchmark's own scorer:
| model | overall | yes/no | choice | score | score ±1 | KL ↓ | Brier ↓ | ECE ↓ |
|---|---|---|---|---|---|---|---|---|
| Ettin-17m, 32-bit (mean of 3 runs) | 0.740 | 0.814 | 0.712 | 0.706 | 0.965 | 0.142 | 0.079 | 0.144 |
| Ettin-17m, 9.7 MB browser file | 0.745 | 0.810 | 0.735 | 0.704 | – | 0.143 | 0.078 | 0.169 |
| nanoGentzen-Jev-v3 (its card) | 0.731 | 0.828 | 0.685 | 0.693 | 0.973 | 0.150 | 0.081 | 0.087 |
"Score ±1" counts a rubric answer as right when it lands within one level of the reference. KL and Brier measure how far the predicted probabilities are from the reference ones, and ECE how far the model's confidence is from how often its top answer is right; lower is better for all three. Ettin is ahead on overall, choice and score accuracy and on KL and Brier. nanoGentzen is ahead on yes/no questions, on score ±1 and on calibration, which is Ettin's weakest number.
It fails on workflows it was not trained on
The fair comparison with Jev is a workflow the small model has never seen. I trained it on three of the four workflows and tested it on the fourth, once for each choice of the fourth.
trained on all four workflows trained on three, tested on the fourth
Data
| model | trained on it | never saw it |
|---|---|---|
| Ettin-17m | 0.740 | 0.353 |
| Monad 56M | 0.751 | 0.333 |
On a workflow it has not trained on, Ettin-17m scores 0.353. The benchmark's own baseline, which ignores the input and always answers each question's most common training label, scores 0.470. Monad does the same (0.333). On 104 questions from unrelated domains (bank customers' requests, agent guardrails, bug severity), where Jev scores 0.894, the fine-tuned Ettin scores 0.279. An outside probe of Jev's behaviour by Archer Hume estimated it at around 10 billion active parameters, about 600 times Ettin's size.
So a browser decision model is a specialist. Pick the questions you care about, collect a thousand or so labelled examples of your real input, and train the small model on exactly those.
Sonnet labelled 1,842 Bluesky accounts for $14.58
Labelled examples are usually the expensive part. Here a large model supplies them, and for the Bluesky questions the teacher was Sonnet.
I collected the accounts from four places, to get a mix: 495 members of Oskar's AI list, 411 accounts those members follow, 399 authors in Bluesky's Discover feed, and 537 authors of English posts seen in three minutes of Bluesky's live firehose, which is where most of the bots come from. For each account the model sees the profile and bio, follower and post counts, how quickly the account posted, and its 12 most recent posts, replies and reposts, each cut to 140 characters.
| question | answers |
|---|---|
| account type | human · bot · organization · news or link aggregator |
| how much is about AI | none · occasional · frequent · nearly all |
| AI role | researcher · builder · commentator · creative · business · not AI-focused |
| attitude to AI | enthusiast · pragmatic · critic · mixed · none shown |
| own writing | mostly reposts and links · mixed · mostly its own words |
| each post: about AI? | yes / no |
Sonnet answered five accounts per request, giving a probability for every option rather than a single pick. I ran it through Claude Code from a script, with no chat window and its tools switched off, six requests at a time. Labelling all 1,842 accounts took 15 minutes. A second pass over 150 of them, in fresh batches, measured how consistent the teacher is; both passes together cost $14.58. Sonnet gave the same top answer both times 88 percent of the time on "own writing", the hardest question, and 97 to 98 percent on account type and on whether a post is about AI.
The student agrees with Sonnet on 81 to 94 percent of answers
I trained the student on 1,542 accounts and tested it on the other 300. Two baselines show how much the training adds. The first always gives each question's most common answer. The second is the method the List Scout page here uses: run each text through the 10 MB Ettin embedder, unchanged, to get one vector per text, and fit a linear classifier on those vectors (in effect a weighted vote over the vector's numbers, with the weights fitted to Sonnet's labels).
most common label classifier on frozen vectors fine-tuned (mean of 3) Sonnet vs itself
Data
| question | most common label | frozen vectors | fine-tuned | Sonnet vs itself |
|---|---|---|---|---|
| account type | 85.0% | 86.7% | 91.7% | 97.3% |
| how much is about AI | 64.3% | 78.3% | 81.3% | 92.0% |
| AI role | 78.0% | 89.7% | 88.1% | 95.3% |
| attitude to AI | 77.0% | 90.0% | 89.7% | 94.7% |
| own writing | 68.3% | 75.7% | 85.4% | 88.0% |
| each post: about AI? | 83.9% | 92.7% | 94.2% | 98.0% |
The fine-tuned model is ahead of both baselines on four of the six questions and lands between 3 and 11 points short of Sonnet's agreement with itself. The gain is largest on "own writing" (85 percent against 76 for the frozen vectors) and on account type (92 against 87). Those answers depend on how the feed is put together, such as the share of reposts and how fast the account posts, more than on any one post's topic. On AI role and attitude the frozen vectors do as well or slightly better: a builder writes about building with AI and a critic about what is wrong with it, so the topic of the posts predicts the answer.
Bots are where it is weakest. Sonnet called 85 of the 1,842 accounts bots, 18 of them in the test set. Across three training runs the model found 5 to 8 of those 18, and when it did call an account a bot, Sonnet agreed 80 to 100 percent of the time. More bot examples in the training data are the obvious next step.
Three hundred labels nearly match a classifier trained on 1,542
Data
| 100 | 300 | 700 | 1,542 | frozen vectors (all) | |
|---|---|---|---|---|---|
| five account questions | 78.4% | 83.5% | 85.4% | 87.2% | 84.1% |
| each post: about AI? | 87.0% | 92.2% | 93.3% | 94.2% | 92.7% |
With 100 labelled accounts the model is already ahead of the most-common-answer baseline on every question but account type, where it ties. With 300, its average over the five account questions comes within a point of the frozen-vector classifier trained on all 1,542. The curve is still rising at 1,542, so adding labels would raise agreement further. At $0.04 per five accounts, another thousand costs about $8.
An account takes 53 milliseconds on a laptop GPU
The page downloads the 10 MB model once and runs it with GPU code I wrote by hand for this model (on WebGPU, the browser's interface to the graphics chip) rather than a general machine-learning runtime. Classifying an account is one batch of 17 short passages: five account questions plus one for each post. On Oskar's MacBook in Chrome that took 53 milliseconds at the median over eight test accounts, including splitting the text into the model's word pieces (about 2,700 of them per account). Fetching the posts from Bluesky takes longer than the model does. The code that turns an account into the text the model reads exists twice, in Python for training and in JavaScript for the page, and the two produce identical text on all 25,589 cases checked.
Other pages can link straight to an account: the Account Decider takes a handle or a DID in its address, as in ?h=austegard.com or ?did=did:plc:….
The model inherits Sonnet's blind spots
The model repeats Sonnet's judgments, mistakes included, and Sonnet saw only what the model sees: a profile and 12 recent items. An account that changed topic last week is judged on last week. Both the accounts and the encoder lean English. The business-workflow results come from a synthetic benchmark whose reference answers are themselves a model's.
The same recipe works for other questions: write them down, have a large model answer them for a thousand or two examples of the real input, and train a 17M-parameter encoder to give the same answers. The code, measurements and write-ups are in browser-decider (the benchmark, the engine, the comparison with Monad) and bsky-account-decider (the Bluesky labels and model). The benchmark version has its own demo page, Browser Decider.