DEV Community

Cover image for Quits: a group tab that remembers who doesn't drink
Kateryna Ivashchenko
Kateryna Ivashchenko Subscriber

Posted on

Quits: a group tab that remembers who doesn't drink

Four flatmates in Kraków go out for dinner. The bill is 412 zł, Clara pays, and 140 of it is wine. Ana doesn't drink. Every split app I've used does one of two things here: it splits evenly and Ana quietly pays for wine she didn't have, or somebody types the line items in by hand. Next week it happens again. Nobody wrote the rule down, so the app never learns it.

That's the whole idea behind Quits: tell it once who doesn't drink, and it remembers. I built it for the Tatum × Walrus "Total Recall" hackathon, and this post is about how the memory works, where Tatum fits in, and what happened when I measured whether it actually learns.

Try it: the live demo needs no wallet and no sign-in. You're one of four flatmates whose debts are real USDC on Sui mainnet.

What it does

  1. You log a bill in plain words. "412 zł dinner at Pod Baranami, Clara paid, 140 of it was wine." A model reads the payer, the currency and the line items. Polish złoty, euros and hryvnia are priced into USDC with Tatum's exchange-rate API.
  2. Quits proposes a split and shows why. "Remembered Oct 14: Ana doesn't drink, so leave her out of alcohol", with a link to the Walrus blob that holds that memory.
  3. You correct it once. "Ana doesn't drink." That sentence becomes a typed rule and a memory. A fact about one night ("Dan wasn't there") fixes that bill and teaches nothing.
  4. Debts settle in USDC on Sui. In one tap from your wallet, or by an agent that pays from a Sui Allowance inside limits the chain enforces.

The split names the memory behind it

The model describes, code decides

The first version I sketched let the model do the split: hand it the group's history and ask for amounts. It works surprisingly well, and it's the baseline I measure against below. But it has two problems for money. You can't tell why it put 6.66 on Ben, and the prompt grows with every bill the group ever adds.

So Quits splits the other way round. The model only runs when someone corrects a split, and its job is narrow: turn the sentence into one rule from a small language.

// "Ana doesn't drink"          -> { kind: 'exclude', member: 'ana', when: { tags: ['alcohol'] } }
// "rent goes by room size"     -> { kind: 'weights', weights: { ana: 40, ben: 20, ... }, when: { category: ['rent'] } }
// "Ben always gets the taxi"   -> { kind: 'covers', member: 'ben', when: { category: ['taxi'] } }
// "whatever you buy for yourself is yours" -> { kind: 'payerCovers', when: { tags: ['personal'] } }
Enter fullscreen mode Exit fullscreen mode

A deterministic engine applies the rules: line items first, then routing rules (the most specific one wins, newest breaks ties), then weights, with a largest-remainder pass so the shares always add up to the cent. Same rules in, same split out, every time. When the model can't turn a correction into a rule, Quits asks back instead of guessing.

Walrus Memory is where the knowledge lives

Every learned rule is a Walrus Memory memory. The text is a sentence a person would write, with the typed rule appended so it can be read back without the model:

Ana doesn't drink, so leave her out of alcohol. ⟦quits:v1 {"kind":"exclude","member":"ana","when":{"tags":["alcohol"]},...}⟧
Enter fullscreen mode Exit fullscreen mode

Rules live in two places. Group habits (rent by room size) go in the group's namespace. Personal traits (Ana doesn't drink, Clara is vegetarian) go in the member's own namespace, so they follow her into every group she joins. A member can also point that memory at their own Walrus Memory account: then the blobs land in their wallet and they can revoke Quits.

The part I care most about is that the memory is load-bearing, not a log. At every split Quits calls recall on the group's and each participant's namespaces and applies what comes back. Postgres only keeps an index. There's a "Rebuild from Walrus" button on the rules page that wipes the index and restores it from Walrus alone (and refuses to touch it if Walrus returns fewer rules than it holds, which is exactly what you get while a write is still landing).

Each memory is a Walrus Blob object owned by the memory account and tagged with the delegate key that wrote it, so you can count them on chain. The app's proof page and pnpm verify:onchain both do that through Tatum's GraphQL endpoint. At the time of writing there are over 60.

A few things I learned the hard way:

  • Writes take 20 to 90 seconds, and there's a rate limit per owner (60 points a minute, 500 an hour). Quits queues memory writes in a worker with a token bucket, so the web app never waits on them.
  • A rule you just taught can be missing from recall for a minute. Quits applies it from the index and labels it "saving", instead of silently dropping it.
  • Blobs expire after a few epochs. Quits doesn't renew them yet. That's in the honest-limits section of the README.

What Quits has learned in one group

Tatum is the gateway to Sui

Every read and write to Sui goes through Tatum:

  • gRPC for balances, objects, simulating and executing every payment, and the settlement agent's transactions. One gotcha: the default gRPC-web transport from @mysten/sui fails against the gateway. A native GrpcTransport from @protobuf-ts/grpc-transport with the API key in metadata works, at 70 to 250 ms a call.
  • SubscribeCheckpoints, filtered to members' addresses, so a USDC transfer someone sends outside the app still closes the matching debt. The filter takes at most ten literals, which I found out when the eleventh member signed up. The worker now opens one stream per ten addresses and staggers them, because the free plan allows three requests a second.
  • GraphQL for listing the memory blobs.
  • The exchange-rate API for every bill that isn't in dollars. If it's unavailable, the bill stays unpriced rather than guessing a rate.

The app's proof page shows the live call log per method, with what answered and what failed.

Paying: x402 on Sui, and an agent on a leash

Every debt is an x402 resource. GET /api/pay/:debt answers 402 Payment required with the exact amount, the creditor and sui:mainnet. The debtor's wallet signs a USDC transfer, sends it back in PAYMENT-SIGNATURE, the server simulates it through Tatum, checks the balance change is exactly the debt to exactly the creditor, executes it once and returns the digest. The same endpoint also speaks MPP.

Payments are gasless USDC transfers (balance::send_funds), so paying a debt needs no SUI. They need a ValidDuring expiration, which isn't obvious from the docs: without it the node rejects the transaction.

Auto-settle is the part I'm proudest of. A member signs one Sui Allowance bound to the Quits Move package. The package, not my server, decides each pull: only a debt that was recorded on chain, only the exact amount, only to someone the member listed, only once, and only after an objection window. The agent learns how much to pay without asking (approve a bigger one and the limit rises, object and it halves), but it can never go past what the member signed. The Move tests cover each refusal.

An x402 payment on Sui mainnet

Does it actually learn?

I wrote a benchmark before I trusted any of this: three simulated groups (a flat of four, a ski trip of five, a couple), four weeks of ten bills each, with habits nobody wrote down. A simulated member corrects a wrong split in plain words, up to twice per bill. I compare Quits with an even split and with the obvious alternative: an LLM given the whole group history in every prompt. Same model, same bills.

The first run went badly. The whole-history prompt beat Quits, because one of the couple's habits ("whatever one of us buys for ourselves is ours") had no rule kind. I added it, and then I had to stop trusting that seed, because I'd tuned the language on it. Every run since is listed in the README, including the ones Quits lost.

On a fresh seed, with nothing tuned on it:

Right the first time Corrections per 10 bills Misallocated if nobody corrected Model tokens per bill Time per bill
Even split 10.8% 17.83 $3,008.16 0 0 ms
LLM with the whole history 90.0% 1.00 $422.00 10,592 20.4 s
Quits 90.0% 1.00 $417.53 269 390 ms

So: as accurate as re-reading everything (the same number of bills right and the same number of corrections, slightly less money in the wrong place), at 39 times fewer tokens and 52 times faster. After its first week in a group, Quits got all 90 remaining bills right the first time.

The number that's really about memory is the second group. When the four flatmates start the ski trip after a month in the flat, the habits they taught about themselves come with them. The trip's first week is right 70% of the time, against 50% for the same trip started from nothing (and 50% for the whole-history prompt), and it needs 0.75 corrections per 10 bills instead of 1.25. A prompt holding one group's history can't do that: every new group starts empty.

The members and their habits are simulated, and the habits are written in the language Quits learns, so this says nothing about rules it can't express ("whoever forgot their wallet last time pays"). The method and the caveats are in eval/README.md.

What's real

Everything in the demo runs on Sui mainnet: the Move package, every payment, mandate and allowance, the memories on Walrus. The demo flatmates' wallets are held by the server so you can pay without one, with small balances and a 1 USDC cap per debt. Sign in with Slush or any Sui wallet and you pay from your own. There's also a Telegram bot and an MCP server, and they read the same memory, so a rule you teach in one place applies in the others.

Top comments (0)