Changelog
Every release the studio has shipped, newest first. Anything marked in grease pencil needs a decision from you before you upgrade.
2026
Back to the tested fits, with deburring in the assembly steps and pegs that find their holes.
v0.2.1 loosened the fits after the first full cube was hard to assemble. The other four panels then slid in fine at the original clearance: the first panel’s brim ridge was the problem, not the groove. The snapped peg came from leaning posts and putting the lid on at an angle, not from a tight hole.
Changes
- Groove clearance back to 0.20 mm and peg clearance back to 0.10 mm, the values a full cube was actually assembled with. The frame is 14.4 mm wide again.
- Deburr every panel edge. The guide and the zip README have a new Before you assemble step: peeling the brim leaves a ridge, and it is enough to make a well-fitting panel bind. The groove test now deburrs its strip too.
- Press the lid straight down. The tall posts can lean out a little until the panels are in; push them upright first, and take the lid on and off as little as possible.
- Self-centring pegs. Each peg tip tapers over 1.5 mm (was 0.6 mm), so it finds its hole when a post leans instead of catching on the rim.
Mixing parts with earlier versions
v0.2.2 parts fit v0.2.0 parts: only the peg tip changed. Parts printed with v0.2.1 should be used together, since that frame is 0.05 mm wider.
Looser default fits, after the first full cube needed too much force to assemble.
Changes
- Groove clearance 0.20 → 0.25 mm and peg clearance 0.10 → 0.15 mm. The test pieces fit well at the old values, but a full cube binds more: panels slide along 81 mm grooves instead of 15 mm, tall posts bow slightly, and the lid seats four pegs at once. At the old values the parts went together only with real force, and a lid peg snapped.
- The frame grows 0.05 mm with the groove clearance (14.45 mm).
- The printing guide and the test-piece instructions now say to pick the best-fitting test piece, then go one step (0.05 mm) looser for the full cube.
Upgrading a cube printed with v0.2.0
If the lid is too tight, reprint just the lid at the new defaults: it fits a v0.2.0 frame and panels. The post holes don’t move; the only difference is that the lid sits 0.05 mm higher, well within print tolerance. Looser panel grooves need a new frame.
Five identical panels, a lid that holds without fragile snaps, and simpler settings tuned by real test prints.
Design
- Every panel is the same 81 mm square with a 5 mm border held in a groove on all four sides, so all five faces are framed alike. The top panel no longer has notched corners and prints standing up like the others.
- The top panel rests on four small corner supports and the lid traps it from above, so nothing falls out if the cube is tipped.
- The lid attaches with printed press-fit pegs or glued magnets. The tiny snap ridges, which slicers couldn’t print, are gone.
- The frame is sized automatically to fit the grooves and the pegs or magnets, with 2.4 mm walls at every size.
- Panels export standing up, for smooth shading with no infill grid.
Settings
- Defaults from test prints: groove clearance 0.20 mm, peg clearance 0.10 mm.
- Fewer, clearer settings, grouped as Cube, Lid, Fit and Lithophane, with nothing that conflicts. Panel thickness follows Max thickness.
- Resolution in pixels per mm (higher = finer), with warnings when it’s coarse, finer than a 0.4 mm nozzle, or makes very large files. Large cubes now really get the detail you ask for.
- Each photo has its own settings: the Edit button on a photo crops it and adjusts brightness, contrast, sharpening and more, with a live preview.
Also
- Every download records the settings it was made with, in
README.txtandsettings.json. - The layout works on phones.
- The printing guide is part of the site, at /guide.
- A link preview image and icons for sharing.
- Fixed: a crash when dragging the crop box quickly; photo thumbnails now show the crop.
The first public beta: five photos in, a 3D-printable lithophane lamp out.
What’s in it
- Five lithophane panels: four sides and a top. Empty slots become plain panels that glow evenly.
- A framed cube: every face shows the same square window. A floor with a cable hole, and a lid that attaches with printed press-fit pegs or glued magnets.
- Image controls: crop, brightness, contrast, auto-contrast, gamma, sharpening, grayscale mode and invert.
- Live 3D preview with an exploded view, and STL export part by part or as one zip, already oriented for printing (panels stand upright).
- A printing guide at /guide, and test pieces that check groove fit, lid fit and image brightness before you commit to a full print.
- Warnings for settings that would produce parts that can’t be printed or won’t fit together.
Beta
The geometry is checked by automated tests. The physical fits are still being test-printed, so default clearances may change. If you print one, please report how it went.
Try it at lithophane-maker.merope.dev.
What’s new
- A long note now scrolls wherever you drag it, including in the space around the photos, tags and buttons at its foot.
Changes
- fix(record): a drag anywhere in a long note scrolls it (1.37.3)
What’s new
- A long note now scrolls inside the notes box, so the account and category stay where they are while you type.
- Transfers show their note in the list, next to the accounts the money moved between.
Changes
- fix(record): a long note scrolls under its bar; transfers show their note (1.37.2)
What’s new
- Peace no longer slows to a crawl when it opens on a day a backup is due. Backups are now made in the background, so the app stays quick while one runs.
Changes
- fix(backup): pack and seal natively so a due backup cannot stall the app (1.37.1)
What’s new
- You can now share a receipt photo. Tap it to open it full screen, then tap Share at the bottom to send it to any app, like WhatsApp or email.
Changes
- docs(whats-new): Share is at the bottom now
- test(e2e): scroll to Backup & export before tapping it
- feat(receipts): Close and Share at the viewer’s foot, in theme colours
- feat(receipts): share a photo from the full-screen viewer (1.37.0)
A spectator no longer holds a lock on games.
A spectator no longer holds a lock on
games. The 0.9.0 migration failed on its lock timeout with every worker drained, behind the live view of a game.Fixed
- The live game stream lets go of the database once it has caught a reader up. It read the
missed events and then kept that transaction open, idle, for as long as the tab stayed open:
ACCESS SHARE on
gamesandgame_events, and one of the API’s 30 pooled connections, per viewer. Nothing after that read touches the database — the live half is Redis alone — so the session is now closed there. The lock blocked anyALTER TABLEon either table, which is how the 0.9.0 deploy failed; the connections would have run the pool dry at about 30 viewers per API process.
Deploying from before 0.9.1 needs the API stopped first, because
deploymigrates while the old API is still serving:docker compose -f docker-compose.yml -f docker-compose.prod.yml stop api, then./chessmark deploy, which starts it again. From 0.9.1 on,deployalone.Published, not deployed: production changes when
./chessmark deployis run on the server. To run exactly this release:./chessmark deploy v0.9.1.- The live game stream lets go of the database once it has caught a reader up. It read the
missed events and then kept that transaction open, idle, for as long as the tab stayed open:
ACCESS SHARE on
Game and model pages say what happened, and only pages with something to say ask to be indexed.
Search Console was holding back hundreds of pages as near-duplicates: game pages whose only text was a move list, and pages for models that never played. A finished game now opens with who played, the opening, how it ended and the moments that mattered, quoted from the models’ own reasoning; a model page shows its record in tables; retired models are listed again; and a model page can start a game with it.
One migration, additive:
0cc5ce1e26e5adds a nullablegames.digest. After deploying, run./chessmark digeststo fill it for the archive; until then each game computes its digest on its first view.Changed
- Pages with nothing to say no longer ask to be indexed
(ADR-0068). A model
that has never finished a game, and a game shorter than 10 plies, are now
noindex, followand left out of the sitemap. That was 272 of the 299 model pages it listed. Model and tournament entries now carrylastmod, the date of their latest finished game.
Added
- Game pages say what happened, in words, and model pages show it
(ADR-0069). A finished game
opens with who played, the opening, how it ended, the material and the cost, and quotes the
models’ own reasoning at the first illegal move, the biggest capture and the end. A model page
shows its results by colour and by ending, its illegal-move rate beside every model’s, a
head-to-head table of every opponent and its most frequent openings. Openings are named from
Lichess’s opening table. After deploying, run
./chessmark digestsso every old game has its digest, and the model pages count every game’s opening. - Retired models are listed again.
/models, the sitemap and the game archive’s filter now include every model that has finished a game here, marked retired when OpenRouter no longer lists it. Twelve such models, Qwen3.8 27B among them, had pages nothing linked to. - A model’s name on a game’s info card links to its page.
- A model page can start a game with it. Play against it and Match it against a model
open
/playwith the model already chosen (?model=…&mode=…), and a signed-out visitor is returned there, choice intact, after signing in. Not shown for a retired model.
Published, not deployed: production changes when
./chessmark deployis run on the server. To run exactly this release:./chessmark deploy v0.9.0.- Pages with nothing to say no longer ask to be indexed
(ADR-0068). A model
that has never finished a game, and a game shorter than 10 plies, are now
Changes
- docs(store): voice shot while it reads; Budgets back in place of Accounts
- docs(store): fresh screenshots with voice and Penny, widgets with the mic
- feat(store): voice and Penny in the listing; the mic hint is just “Say it your way.” (1.36.1)
What’s new
- You can now speak a record instead of typing it — in Arabic, English or both. Tap the mic on a new record, use the new Speak shortcut or widget button, or tap the mic in Penny to ask her a question out loud.
- Voice uses your Gemini key. Peace asks for the microphone the first time you tap it, listens only while the mic is open, and never keeps the recording.
Changes
- chore(voice): allow the microphone, and declare audio in Data safety
- fix(voice): silence is never sent, and a model that heard nothing says so
- fix(voice): a permission check no longer cancels the mic it opens
- feat(voice): speak a record, or a question to Penny (1.36.0)
What’s new
- Peace now opens in your theme: light for Paper and Mist, dark for Ledger and Graphite, instead of always dark. On Android 12 and later, a new theme shows from the next time you open the app.
- The Analysis chart is bigger. Tap or hold any part of it to see which category it is and its share of the month.
Changes
- feat(theme): the splash opens in the chosen theme (1.35.0)
- docs: refresh the screenshots for the new Analysis screen
What’s new
- The Analysis chart is bigger. Tap or hold any part of it to see which category it is and its share of the month.
Changes
- feat(analysis): a bigger ring you can touch (1.34.0)
What’s new
- Analysis now breaks each category down by its sub-categories, in the list and as an outer ring on the chart. Every category gets its own row too, instead of the smaller ones being folded into “Other”.
Changes
- feat(analysis): break each category down by its sub-categories (1.33.0)
What’s new
- Fixed a rare freeze when Peace opened while a Google Drive backup was due. If Peace ever stops responding again, About now lists when and what it was busy with, so you can share it with us.
Changes
- fix(backup): stop the Drive catch-up freezing the app at launch (1.32.1)
What’s new
- The keypad’s next key now shows where it goes: the account, then the category, then a check to save.
- After an update, Peace now shows what changed — like this. Find it again any time in About.
Changes
- feat(record): the next key shows what it opens — account, category, save (1.32.0)
- site: bank messages get their own feature, from a real frame of the promo clip
- ci(play): also release each upload to internal testing at once
What’s new
- After an update, Peace now shows what changed — like this. Find it again any time in About.
Changes
- feat: What’s new after an update, from one release-notes file (1.31.0)
Changes
- fix(setup): Continue on the AI step when a key is already saved (1.30.1)
Changes
- feat(setup): choose a theme during onboarding, by looking (1.30.0)
Changes
- feat: read PDFs and text files into a record, like receipt photos (1.29.0)
- docs(store): widgets on a home screen — store screenshot 3 and a site section, from real captures
Changes
- fix: centre the app in a 640dp column on tablets and unfolded foldables (1.28.1)
- docs: listing and site name widgets, shortcuts, the in-app camera, import, lock and themes; Arabic and Peace Cloud deferred
Changes
- feat(widgets): Balance is 4×1 by default; taller, its accounts fill the space
- feat(widgets): Budget and Balance as separate widgets; Balance lists accounts when stretched
- feat: home-screen widgets — This month and Quick add (1.28.0)
- fix(budgets): a set-aside limit leaves the spent total too
- docs(screenshots): retake Penny’s three in Ledger’s red; fix the script’s two stale waits
Changes
- feat(shortcuts): Penny takes Income’s launcher slot (1.27.0)
Changes
- ci(play): upload the bundle to a testing track as a draft, keyless via WIF
- build: R8 shrinking and obfuscation in release builds (1.26.1)
Changes
- feat(record): the notes field becomes the camera; one press attaches (1.26.0)
Changes
- docs: shortcuts and accessibility in AGENTS.md and the release plan
- feat(a11y): spoken keypad, one-sentence chart rows, named swatches, large-text density
- feat: launcher shortcuts for Expense, Income and Receipt (1.25.0)
- docs(release-plan): the OAuth consent screen is in production
- docs(release-plan): Play App Signing, Drive for play, Data safety and the internal track done
- docs(screenshots): retake with Ledger’s red (1.24.0)
Changes
- feat(penny): Your rules, appended to her system prompt (1.24.0)
- fix(setup): the accounts step shows and sets today’s balance, not the opening one
- fix(setup): fade the step emblem in instead of springing it
- fix(theme): Ledger’s expense back to the salmon red
- docs(store): the filled-in Data safety form, importable into Play Console
Changes
- fix(site): generate the theme table before typechecking
- feat(themes): a grid of live previews, and a themes section on the website
- fix(restore): name the backup file in the confirmation, not its document id
- feat(themes): Ledger, Graphite, Paper and Mist, or match the phone (1.23.0)
- chore(site): drop a generated file committed by mistake
- feat(site): /delete/ — the page Play’s Data safety form links to
Changes
- feat(import): bring records in from a CSV in Peace’s own format
Changes
- feat(lock): app lock, with the phone’s own fingerprint or PIN
- feat(site): the sitemap finds its own pages, with dates
- feat(site): /move/ walks the GitHub version over to Play
Changes
- fix(settings): the index calls it Money & months, like its page
- docs(agents): components read the period label through usePeriodLabel
- fix(periods): labels follow the start day under the React Compiler
- feat(periods): months that start on payday, and a first day of the week
Changes
- feat(settings): “Your rules” is always open, and saves as you type
- fix: setup’s bank step notices access granted; Penny leaves the side menu
- fix(settings): one line above Receipts’ rules, Bank follows the disclosure back, Settings near About
- fix(ai): no key means every AI switch is off and muted
- feat(settings): a dropdown for the model, and Receipts and Bank switches like Penny’s
- feat(settings): AI on the Settings page with a master switch; Recording joins keypad and default account
- feat(settings): AI is one hub — the key, then receipts, bank messages and Penny
- feat(settings): an index of sections, each a page that says what it is for
Changes
- feat: empty states that teach, and About links to the site and the source
- feat(site): /store/ is a page of the listing’s graphics, not a 404
Changes
- feat(store): Penny’s screenshots in dollars too
- feat(store): the Play listing, neutral screenshots, and both put to work
Changes
- style(setup): restore the house formatting the key-check commit overwrote
- fix(setup): check a Gemini key before saving it, and show errors on the field
- feat(setup): onboarding — welcome, seven skippable steps, and the bank disclosure
- docs: update effective date and support email in privacy policy
- ci(play): the shared build makes a signed AAB for flavor play, on a manual workflow
Changes
- docs(permissions): point at the policy’s home in site/content
- fix(android): request 9 permissions instead of 33, and fail the build on an unapproved one
- chore(seed): Telephone becomes Phone; the prompts and defaults are reviewed for regional assumptions
- feat(site): social and meta tags, generated social cards, and the taglines
- feat(site): peace.merope.dev — a static Next.js site on GitHub Pages
- docs(site): privacy policy and Data safety answers for peace.merope.dev
- docs(release-plan): tick off what 1.15–1.16 shipped, and pin the disclosure to before the first upload
- test(e2e): the reminders flow scrolls to each row instead of assuming it is on screen
Changes
- fix(penny): a key saved in Settings reaches Penny’s card at once
Changes
- fix(ui): sheets and snackbars clear the three-button navigation bar
- ci(e2e): run Maestro from one script line, or the workflow never reaches a flow
- test(e2e): one record lifecycle flow instead of three that each logged it first
- test: share DB fixtures, drop tests that only re-proved another, and stop sleeping
Changes
- feat(build): oss and play flavors, and the app moves to dev.merope.peace
Changes
- chore(copy): examples and hints that do not assume one country’s banks
Changes
- feat(keypad): choose 1-2-3 or 7-8-9 on top, and which side the operators go
- docs: Play Store release plan
32 commits since v0.7.0.
32 commits since
v0.7.0. Chessmark is in beta, which is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.Ratings are rebuilt on Bradley-Terry, chat and decision models are ranked apart, and a model’s reasoning level is now part of what plays. The full changelog has every entry.
Ratings, rebuilt
Ratings are Bradley-Terry, and tables are ordered by proven strength. Every game is fitted at once, so the same games give the same ratings in any order, and a model that stops playing keeps what it proved. Tables are ordered by the rating less twice its ±, so a three-game streak no longer outranks a long record. (ADR-0060)
Every rating on the site is one stored run. The leaderboard, a pool’s standings and the matchmaker all read the same fingerprinted run, so what the matchmaker pairs on is what the site shows. The stored leaderboard is now actually stored: a warm read went from about 50 ms to 8 ms. (ADR-0061)
Chat models and decision models are ranked separately, and a game with a person, or between a chat and a decision model, is never ranked. (ADR-0063, ADR-0064)
How hard a model thinks is part of what plays
A contestant is now a model at a quantization and a reasoning level: the same model at high reasoning and at low are separate entrants, rated separately. Until now no request named a level, so every model reasoned at whatever its provider defaulted to, and nothing recorded it. Every game now records the level each seat played at, shown on the game page, the leaderboard and the model page. Starting a game offers the levels a model supports under think, with its default selected. Tournaments and pools can be set to a level; existing ones carry on at each model’s default. (ADR-0067)
Games already played are labelled from whether each seat actually reasoned, read through the level its model uses by default today, and marked inferred on the game page.
Choose who serves a model
Starting a game, against a model or between two, now offers a via menu listing every host for the model with its uptime. auto still picks the most reliable one. The quantization row is now always shown, even when there is only one. (ADR-0066)
Around them
- Download any game as a GIF, drawn in your browser, one frame per move. The game page’s buttons are now icons. (ADR-0065)
- Decision models with a limit on options play in heats, then a final between the heat winners, so Tev and Solar can play a middlegame. (ADR-0059)
- Standings show abandoned pairings, as W/D/L/A.
- A failed call is on record, with the provider’s error body, in the turn inspector. (ADR-0062)
- A game interrupted by a deploy resumes within a minute, not fifteen, and a turn longer than fifteen minutes is no longer run twice.
- A new game appears on the site as soon as it starts, not at its first move.
- On a phone, the leaderboard and standings scroll sideways instead of hiding columns, and the play page no longer scrolls sideways.
- Fix: “sign in” no longer sometimes shows “That did not load.”
Deploying
Three migrations, all additive:
8eac4a8e7efa(model_registry.decisions_max_choices),9a854789833f(leaderboard_snapshots.scope) and8ddbdc635007(the reasoning columns onmodel_registryandmodel_endpoints).Deploy by hand with
./chessmark deployonce Publish images has finished. Then expect:- every rating to change once, on the first read after the deploy, because the method changed. It is not a regression;
- the leaderboard to reshape once more when the catalogue refresh labels past games with their reasoning level. Until it finishes, new games play with no level and are labelled with the rest;
- no decision model to be paired until the same refresh re-checks each one under harness
d2.1.
Changes
- feat(assistant): replies render as one native text view, so a selection spans the whole reply
- fix(assistant): a horizontal rule is a paragraph break, and italics around a figure lose their asterisks
- refactor(assistant): one general query instead of a special tool, and no Copy button
- feat(assistant): fewer steps, no step limit, and replies you can copy
Changes
- feat(assistant): Penny can refund a purchase and reverse a transfer
Changes
- docs: a row of Penny in the README, and npm run shots:penny to regenerate it
- feat(assistant): Penny’s coin is the cursor, and the label says what is happening now
- test(assistant): live flows return to Penny before attaching, and back out of the folder picker
- docs: Penny in the features, and in the list of everything that talks to Google
- feat(assistant): the assistant is called Penny
- feat(assistant): attach receipts and invoices, save reports, richer reports, retries
- feat(assistant): replies stream in, the dots say what is happening, and a real new-chat icon
- chore: npm run demo:ledger — a year of invented data, restorable as a backup
- fix(assistant): an expired card does not leave the composer asking for an answer
- fix(assistant): what the device runs showed, and E2E flows with and without a key
- test(assistant): a live suite against the real Gemini API, skipped without a key
- feat(assistant): the chat screen, its charts, cards and settings
- feat(assistant): the engine — tools over the ledger, cited figures, approval-gated writes
24 commits since v0.6.0.
24 commits since
v0.6.0. Chessmark is in beta, which is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.Credit can be bought in any amount, and tournaments are paid for by Chessmark’s own house account. The full changelog has every entry.
Buy any amount of credit
/creditsells any amount from $5 to $100, with $5, $10 and $25 as quick picks. The amount you choose is what you pay, tax included. Each purchase is shown as one sum: tax, the payment processor’s fee, 5% for running Chessmark, the AI provider fee (OpenRouter’s 5.5% on the credit we buy from it), and the credit left. Where there is no tax, $5 buys $3.79, $10 buys $8.05 and $25 buys $20.85. The checkout opens in a dialog inside the credit page and closes itself once the payment goes through. (AUTH-22, ADR-0056)Credit is sold only while OpenRouter can pay for it. A checkout reserves its credit against OpenRouter’s remaining balance, less what users already hold and a $10 reserve. If an amount does not fit, the page offers the largest one that does. (AUTH-23)
Sales are a switch, and it starts closed.
./chessmark sales open | pause "reason"opens and pauses sales without a restart. While sales are paused,/creditshows the breakdown and “not on sale right now”. Anything already paid for is still credited. (AUTH-25, ADR-0057)The house pays for tournaments
Every game no person started is now charged to Chessmark’s house account, turn by turn. When the house runs out of credit, those games pause, showing “waiting for Chessmark to add credit”. A tournament with paid models waits rather than starting games it can’t fund. Credit in the house counts against the credit for sale, so tournaments can no longer spend the OpenRouter balance that backs users’ credit. (AUTH-26, ADR-0058)
Move sounds
A move, a capture, castling, a check, a promotion and the end of a game each have their own sound, live and in replays. Sound is on by default. “sound on / off” beside “copy link” turns it off, and your browser remembers the setting. Sounds by Kenney.
Around them
./chessmark purchases <email>lists an account’s purchases and whether each can be refunded. (AUTH-24)- Credit unused for 12 months may expire, because OpenRouter can expire the credit we hold with it after a year.
- Fix: a tournament whose last game is paused is no longer marked finished.
Deploying
Two migrations, both additive:
credit_reservations, and the house account’s user row.Deploy by hand with
./chessmark deployonce Publish images has finished. Then:- fund the house with
./chessmark credits house <usd>. It starts empty, and tournaments wait until it has credit; - open sales with
./chessmark sales openwhen ready. They start paused.
24 commits since v0.5.0.
24 commits since
v0.5.0. Chessmark is in beta, which is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.Credit is money now. It is spent at what each turn actually cost, settled to what OpenRouter billed, and ready to be bought. The full changelog has every entry with its evidence.
Credit is dollars, spent at actual cost
A balance is US dollars. Each model turn is charged its real cost as it is played, from the provider’s own token counts, with no estimate and no hold. When credit runs out, a paid game pauses before its next turn and resumes when credit is added. Playing a free model needs no credit at all. You set a game’s spending limit yourself, or none, and you can pause and resume the model-vs-model games you pay for. Existing balances were reset to zero. (AUTH-10–17, ADR-0052)
Every game is settled to what OpenRouter billed
Each game is checked against OpenRouter’s own bill for its session, about 5 minutes, an hour and a day after it ends or is held. Its payer is then settled to that figure, as a charge or a refund. The page shows Billed, with a tooltip that explains any difference from the running total. On production’s copy, 168 games reconciled to $0.4332, against the key’s own $0.4334 for the month. (OPS-25, AUTH-18, ADR-0054)
Finding the gap fixed its main cause. A failed turn now keeps the answers it paid for, where it used to roll them back and pay for them again: about 3.5% of successful answers since mid-September. A turn that crashes spends one of its attempts instead of looping. (ADR-0053)
Buying credit
/creditsells packs of $5, $10 and $25 that grant $4.00, $8.50 and $22.00. Each is shown as a sum: the price, the payment processor’s fee, 5% for running the site, and the credit that is left. Tax is added at checkout. Paddle sells the credit as merchant of record. Only its signed webhook moves a balance: once per purchase, for the amount the server maps the paid price to. Refunds and chargebacks take the credit back. The terms of service, privacy policy and refund policy are linked from every page. (AUTH-19–21, ADR-0055)Buying is off until Paddle approves the live account. Until then the packs show “not on sale yet”.
Around them
- Starting a game has its settings on one row: spending limit, ply cap, and Talk. For a game between two models, Talk is trash talk, which the form now lets you turn off.
- Your profile lists the games you started, with what each cost.
- Decision models choose what to do with their turn, and are checked before they are offered (harness
d2). (ADR-0051) ./chessmark statuslists turns that crashed, and the worker keeps running rather than dying with them.- Fixes:
- a game ended by its spending limit now says so live;
- balances show enough decimal places to change and to add up;
- pauses fold only into the one directly above them;
- a resumed turn no longer reuses a tool call’s sequence number;
- a tournament’s entrant count now matches its table.
Deploying
Four migrations, all already applied on production. Three are additive. One is not:
1b65e05c166cmade credit dollars and reset every balance to zero.Deploy by hand with
./chessmark deployonce Publish images has finished. Reconciliation needsOPENROUTER_MANAGEMENT_KEYon the server. Buying needs the Paddle settings in DEPLOYMENT.md.39 commits since v0.4.0.
39 commits since
v0.4.0. Chessmark is in beta — that is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.Three things a reader can now do that they could not: watch a decision model play, find any game in an archive, and see a pool that has finished its work stop. The full changelog has every entry with its evidence.
Decision models play
OpenRouter’s decision models — TypeSafe’s Jev 1.13 and Kev 4B today — play through the Decisions API: one request per turn with the position and every legal move described, and a probability back for each. They resign, offer, accept and claim draws as a chat model can, share the leaderboard behind a badge, and get tournaments of their own. The timeline draws each decision as the moves it weighed, withheld from a person mid-game like reasoning. The harness is versioned separately (
d1), and a game is held only to the versions of the harnesses that played in it. (AGENT-23..25, BENCH-13, ADR-0049)Every game, at
/gamesFilter by status, result, ending, ranked or not, model against model or a person at the board, a model, a matchup or an event; search either seat’s name or its OpenRouter id. Every filtered view is a link, and it unfurls as that filter. Paged by keyset, so a game starting mid-read never repeats a row, and
GET /gamesstays two statements whatever it is asked. (UI-12, ADR-0048)A pool can stop
--games-per-pair Ngives a pool a target per era; once every pair has played it, the pool idles until a new model is admitted and then plays only the newcomer’s pairs. (BENCH-14, ADR-0050)Around them
- Pages arrive whole, and the skeletons are gone. Every public read is cached and tagged, and the worker invalidates the tags when a game moves (ADR-0046). The lobby went from complete-at-43ms to 14ms.
- Our own sign-in, sign-up and profile, and Clerk off every page that does not need it: 619 KiB → 258 KiB for a signed-out reader on
/leaderboard. Two live bugs were found by writing the tests — nobody could sign up, and no display name could be saved. - A rebuilt lobby: a podium for the top three, one real turn from a finished game, what a tournament is, and the human-versus-model record with what the opponent has been caught doing.
- Accessibility at 100 on every public page against production’s data, with Lighthouse budgets in CI.
- Fixes that changed a result or a reading: a reopened game now tells both players it reopened (a model had been forfeited for correctly saying the game was over); colours alternate even when games don’t finish; a halted game says when the halt lifts; “retrying shortly” no longer says so forever; links to a model no longer prefetch ~90 times in twelve seconds.
Deploying
Three migrations, all additive:
runtime/decision_versioncolumns, a nullabletournaments.games_per_pair, and a GIN trigram index onplayers.display_name, which createspg_trgm— a trusted extension since Postgres 13, so the database owner can create it without a superuser.A reminder names your verb, not the model’s.
Changes
- fix: name the person’s verb, not the model’s
Ask what the day cost, and hear what is still waiting.
Changes
- feat: ask what the day cost, and say what is still waiting
An error says what to do about it rather than what cannot happen.
Changes
- fix: say what to do, not what cannot happen
One long press, one archive, one way to delete — and a search that keeps your place when a deletion moves you.
Changes
- feat: hold a row in search, say why a delete sent you there, and ask about updates
- test: there is no form to cancel any more
- test: anchor the category scrolls instead of guessing the offset
- fix: say it where the tap was, and lift the FAB clear of it
- feat: one long press, one archive, one way to delete
Archiving is finished: categories go away and return, an archived account keeps its money, and what is put away can be deleted once nothing points at it.
Changes
- chore: 1.9.0 — the rest of the Archived section
- test: reach a tab from Settings, and a button from its card
- ci: run the flows on a runner when there is no emulator to hand
- test: check every flow’s testIDs without a device
- feat: delete what has been put away, once nothing points at it
- fix: an archived account still holds its money
- feat: put categories away, and bring any of the three back
An archived account can come back.
Changes
- feat: bring an archived account back
A backup from any schema this app has ever had restores cleanly.
Changes
- test: restore a backup from every schema this app has had
- fix: an old backup is not a stranger
Five commits since v0.3.0.
Five commits since
v0.3.0. Chessmark is in beta — that is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.Two halves, unrelated in the code and identical in the working habit that produced them: both were things nobody could see from where they were standing. The full changelog has every entry with its evidence.
A harness ceiling stopped counting as a result
db/tournaments.settlesaid in its own docstring that a game the harness stopped “is marked abandoned rather than scored”, and then asked a question that could not answer it: it readGameStatus.ABORTED, and onlyABANDONEDarrives that way. Aply_cap, abudget_exceededor anadjudicationends a gameFINISHEDcarrying a realGameResult, so all three fell through and were scored like any other draw.pool-freeround 175 is what that cost.ling-3.0-flash-santereached8/6P1/1k5P/5K2/5p2/8/8/8 w— a pawn on g7,g8=Qon the move, the black king stranded on b6 — and the 300-ply cap drew it. The rating rules excluded the game correctly the whole time, which is exactly why nothing showed: the two numbers agreed everywhere except one column, and there the page read0.5besideunrated.settlenow asksHARNESS_TERMINATIONS. That set was already the answer and nothing linked it —test_classification.pyexists because three sets classifying terminations had drifted apart once, and this was the fourth, one module away and unchecked.It is also what makes the ply cap legitimate at all. The cap is not stated in the system prompt, and invariant 12 says a rule that decides a game must be. A ceiling that never decides a scored game satisfies that by construction.
The site works on a phone
Seven pages measured at 390px. Every failure had the same shape — a flex row splitting a width that does not exist — and none of it was visible from a desk.
was now a pairing’s two model names 37px each, against the 310px the longest needs stacked, a full row each the pool’s standings column 37px 173px a player’s nameplate 157px of a 209px name the whole row, captures below the leaderboard’s rating behind a horizontal swipe on screen the replay transport 28×28 44×44 on touch, 28 with a cursor the conversation panel as tall as the game was long 58svh, scrolling inside itselfthe stats on a game page a whole conversation below the board a tab Held by a new
mobilePlaywright project that CI runs besidepublic. Eight of its nine assertions fail without these changes.And one the phone work introduced
GameLayoutorders its slots twice, andlg:order-nonelet DOM order stand: the grid was still three columns, the three tops still aligned, and themin()middle column that exists to size the board went to the conversation while the board sat in a 323px rail.replay.spec.tsnow asks which child is in which column, because everything short of that passed.Twenty-nine commits since v0.2.0.
Twenty-nine commits since
v0.2.0. Chessmark is in beta — that is what0.xmeans under SemVer: anything may change, and the public API should not yet be considered stable.One thread runs through this release. A provider that stops answering no longer destroys the work it interrupted — and everything downstream of that discard, from the cooldown ladder to the way a pause is drawn on the page, was built around it. The full changelog has every entry with the evidence behind it.
A turn keeps the rounds it completed
A turn ran inside one transaction and a provider failure raised out of it, so a turn refused on its third call discarded the two that had already been answered and billed. The retry then paid for them again. In
f129b600the rolled-back turns are the gaps in the id sequence — 7854-7856, 7859 — each a fresh attempt redoing the board read the one before it had finished. A model that could not complete a whole turn inside one provider window never banked a step, and therefore never moved.The retry now continues the turn (ADR-0045). Most of that was free: the transcript is rebuilt in
seqorder, so committed rounds are simply there and the model carries on mid-conversation. What needed writing is what must not repeat — the turn prompt andturn_startedare per turn, not per attempt.Which failures keep their rounds follows one rule: commit when the next attempt sends the same request again. A rate limit, a timeout and a 5xx do.
NoRoomToAnswerError,HarnessCeilingError,ProviderAccountingErrorandProviderMangledErrorstill roll back, because each needs the next request to be different.The rollback was avoiding something real — a half-written turn can leave an assistant message whose
tool_callsnothing answered, and that shape corrupted 242 rows across 14 seats — but a refusal comes out ofcomplete(), before the round’s assistant message is appended, so the transcript is already at a clean boundary.The ladder measures the endpoint, not the turn
A turn is many calls against a growing transcript, so an endpoint that answered the board read and was refused on the move never reached the reset: 60s, 300s, 900s, up to the hour cap, against an endpoint that had never gone away. The direction was the perverse part — the longer the game, the less likely a turn completes, so the ladder was harshest on exactly the endpoint a long game most needs. It resets on an answered call now (ADR-0044).
The endpoint being credited was also the wrong one: OpenRouter’s
providerfield is absent for several models — every call inf129b600came backprovider: None— so the clear landed onmodel|*while the strikes sat undermodel|BaseTen. The seat’s pin is the answer when the response has none.A pause is drawn where it happened
The move divider closes its turn instead of opening it, so a reader meets the thinking, the tool calls, then the move — above, a pause belonging to the next seat appeared under the previous seat’s block, which is how a deepseek rate limit read as GLM’s problem. A pause names the seat it is waiting on. And a pause inside an open turn is a step of it, in sequence under the step counter, so unrolling the steps shows what survived the interruption and what followed it.
Then the panel had to be taught that a turn can be committed and still unfinished: an interrupted turn and the retry continuing it were drawn as two headers for one seat, and a turn that talked after it moved was split in half by its own move divider. Both are one turn again, and the frames that predicted a round no longer linger beside the record of it.
Four kinds of event —
game_paused,game_resumed,outputandcompacted— were never reaching the browser at all. The SSE listener list was written by hand and named nine of thirteen types; every one of those events was published, delivered and discarded, which is why a pause appeared only after a reload. The list is keyed by the type union now, so the next omission does not compile.Also
A game already paused when the free allowance ran out could not say so —
9b4bced5sat fifteen hours advertising a reason that had stopped being true within the hour. The reconciler now publishes the events it writes, instead of committing agame_resumedthat reached nobody. And CI can run its jobs again:setup-uv@v10does not exist, and the browser job now stops the servers it starts.Seventy-seven commits. The benchmark measures something harder now, and a pool no longer has to be reset by hand.
The task changed
get_legal_movesno longer says which move is mate. Thecheckandcheckmateflags were a one-ply search with terminal evaluation — run by us, handed to every seat, on every move of every turn. No model playing Chessmark had ever had to find mate in one. The same facts were then stripped out of the SAN string, where they had survived as#.- A third version of the prompt. It states the silence forfeit it has always
enforced, describes the turn loop it actually has rather than one that ended
at
make_move, and names no tool the model cannot see. Three rules that decided games were not in it. - A pool balances its pairings. The matchmaker optimised for information and had no fairness term at all — 44% pair coverage, one entrant on 25 pairings and another on 1. Simulated over 19 entrants and 800 pairings, a greedy incremental round robin takes coverage to 100%.
Harness bounds stopped becoming findings about models
- A turn ends when the model stops, not when it moves — and a turn could previously end between a tool call and its result, leaving a transcript every provider refuses for the rest of the game. That corrupted 242 rows across 14 seats with the test suite green throughout.
- The window is sized for the request going out, not the previous one.
- A halt does not spend a game’s patience. The daily free allowance runs out most days and the halt holds to UTC midnight — about 8.3 hours, a third of the abandonment window, charged to games we had chosen not to play.
Upgrading
Four migrations, all safe to apply with the old code still running. There is no backfill. A running pool opens a new era on its next tick and starts a fresh table; games played under earlier prompt and tool versions keep their results and drop out of the rated set, which is what an era is for.
Phase 0 — toolchain, design tokens, content pipeline, and a verification loop.
Added
- Next.js 16 on a fully static export, with both themes resolving before paint.
- The magnitude scale, doubling as the project status system.
- A build-time content layer with schema-validated frontmatter.
- A screenshot harness, so changes get looked at rather than assumed.
The Now card explains itself and splits by time as well as by account, and a loan stops being filed as a card.
Added
- Now explains itself. The header’s running position reconciles exactly with the Accounts total, and the card now shows the working rather than asking you to trust the number.
- The breakdown splits by time as well as by account.
Fixed
- A loan is not a credit card. It was being drawn as one, with a credit limit it does not have.
Tags — the axis categories cannot be. Many per record, crossing categories, and archivable when the project they name is over.
Added
- Tags. A category answers “what kind of spending was this” and can only answer once. A tag answers “what was this for”, as many times as it needs to, across categories — so “where did the kitchen money go” is still a ring, and the tag can be archived when the kitchen is finished.
Upcoming payments lifted above the ledger, where a thing you have not paid yet belongs.
Changed
- Upcoming occurrences sit above the records list rather than below it. A payment you have not made is a decision waiting, not history.
- Nine screenshots in the README, all generated by a flow that asserts the totals it is about to photograph — so a wrong screen fails the build rather than being quietly captured.
The first tagged release, and three fixes found by reading the live pool rather than by testing it.
0.xmeans what it says: Chessmark is in beta, and anything may change.Fixed
- The pool no longer re-pairs a fixture it can never play. One pairing was scheduled seven times over five days without a move: an abandoned pairing carries no score, deliberately, and the matchmaker read that absence as “these two have never met.” Attempted pairings now count as meetings.
- An endpoint’s output ceiling is no longer a finding about a model. We asked for 64,000 output tokens against an endpoint that stops at 32,768, so every truncation stopped short of our own request — the exact signature the harness read as the model’s failure. It cost one model a game it had already won.
- A forfeit flag follows the game’s ending, not the turn’s status. Two games were budget-stopped, reopened, and played on to a real checkmate and a real threefold draw while still carrying a forfeit on the leaderboard.
Added
./chessmark repair-forfeits, which reconciles forfeit flags against the game record. It reports by default and changes no result.
Reverse a transfer, let a refund say what it refunds, and hide every amount behind one tap in the header.
Added
- One tap hides every amount. The eye in the header masks each figure on every screen, for reading your ledger with someone beside you. It persists, so the eye stays in the header as the way back. Shoulder privacy only — it does not touch screenshots or the recents thumbnail.
- Reversing a transfer, and a refund that names the purchase it undid.
Fixed
- Search finds a transfer by where it landed, rather than by a category a transfer cannot have.
- A reading fills the note in; it no longer overwrites what you had typed.
- Pickers open on the current selection instead of at the top.
- The update-balance sheet and the budget editor both sat under the keyboard.
Eight fixes to the reader after a week of real use — a cash withdrawal is a transfer, not an expense — and a relicence to GPL-3.0.
Fixed
- A withdrawal is a transfer. Cash out of a machine has not been spent; it is spent later, one purchase at a time. Filing it as an expense counted it twice and left the cash account permanently empty. Both halves looked individually plausible, which is why it survived a glance.
- Bank messages get a category. The prompt never asked for one, so every bank record arrived uncategorised — which read as the model declining to guess, and was the app never putting the question.
- The currency the message named is the currency entered. The screen took
its currency from the account and nothing else, so a correctly-read
USD 50.00was saved as fifty pounds. Not displayed wrong — saved wrong. - The message survives a failed reading. The app had the bank’s own words, showed them in the grey row, and threw them away at the one moment they became permanent.
Changed
- Relicensed under GPL-3.0, so anything distributed from this comes back rather than disappearing into a closed fork. The name and the icon stay reserved.
Bank notifications read into records for approval, editable prompts, and a Settings screen that holds your rules rather than the app’s wiring.
Added
- Bank alerts captured and offered as records to approve, never written
behind your back. Android hands over the notification your messaging app
already showed — the same text you read off a lock screen. No
READ_SMS, no conversation history, and the sender list starts empty. - The finance half of each prompt is yours to edit, so “the card ending 0042 is Kenana” resolves against your real accounts.
Changed
- Captured messages join the records list instead of living on a page of their own.
- Two permissions the app never used are no longer declared.
Fixed
- A busy model no longer burns the message it failed to read.
- The app says when messages are waiting on a key you have not supplied.
- Bank alerts captured and offered as records to approve, never written
behind your back. Android hands over the notification your messaging app
already showed — the same text you read off a lock screen. No
Daily totals in the records list, and a settings row deleted rather than wired to nothing.
Added
- A total on each day heading, so the month reads as a sequence of days that each net themselves.
Fixed
- The day heading was clipped by the total sitting beside it.
First light. The whole ledger in one version — keypad entry, budgets, analysis, credit cards, refunds, backups, receipts and recurring payments.
The version stayed at 1.0.0 for seventy-five builds while the app was built, so this is less a release than the shape of the thing when it was first worth installing.
Added
- Recording — a calculator keypad, two-tier categories, transfers written as one atomic pair, refunds that net against the purchase they reverse, and multi-currency with the rate stored per record.
- Making sense of it — records grouped by day and netted, a category ring with a ranked breakdown, budgets suggested from what you actually spend, free-text search with a total, and balances derived from the ledger rather than stored.
- Credit cards as money owed rather than an emptied wallet, with per-card fee profiles and balance corrections that move your position without counting as spending.
- Keeping it — a backup that is a plain zip you can open anywhere, an undo for the last restore, optional Drive backup sealed with a passphrase, and a CSV export that carries both legs of a transfer.
- Receipts read by Gemini, with your own key, off until you supply one.
- Recurring payments and portfolio rebalancing, the latter deliberately not wired into the ledger — it never tells you to sell.