Tally
A voice-first shared household ledger: record who paid for what, and get back the fewest payments that settle everyone up.
Record an expense (
record_expense) — "Sam paid 132 for dinner, split with Chris and Maya"; supports equal splits, weighted shares, or exact per-person amounts, with an optional payer and idempotency key for safe retries.See where things stand (
show_balances) — every member's net position plus a minimal settlement plan (no more than n−1 transfers).Log a repayment (
settle_up) — "Chris paid Sam back 44."Undo a misheard entry (
undo_last) — reverses the most recent expense or payment (flagged destructive).Set up a household (
start_household) — create a named ledger with a currency, putting the speaker in as first member (with invite codes for others).Add flatmates (
add_person) — including nicknames speech recognition might produce.Ask a personal question (
what_do_i_owe) — what the speaker owes one named person, or their overall position.Review history (
recent_activity) — the last few entries (limit up to 25).Speak or show — ledger tools return a spoken sentence plus structured balances/settlements, and a
ui://tally/balances.htmlbalance sheet renders on screen devices; an Echo Dot just loses the picture.Stay correct and safe — integer minor-unit money, balances always sum to zero, exact or unambiguous-prefix name matching (asking rather than guessing), OAuth 2.1 account linking, and a read-only
tally://householdresource.
Enables Amazon Alexa+ to manage a shared household ledger by voice, including recording expenses, checking balances, settling debts, and managing household members.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Tally Sam paid 132 dollars for dinner, split with Chris and Maya"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Tally — a shared household ledger you talk to
An MCP server for Alexa+ that tracks who paid for what in a household, and works out the fewest payments that settle everyone up.
"Alexa, Sam paid 132 dollars for dinner, split with Chris and Maya."
→ Recorded 132 dollars for dinner, paid by Sam, split 3 ways.
That's 44 dollars each.
"Alexa, who owes what?"
→ 2 payments settle Apartment 4B. The biggest: Maya owes Sam 44 dollars.Try it — no Echo needed
tally-playground.onrender.com — press the ring and talk (or type). You are Sam in a private sandbox household with Chris and Maya; nobody else sees it.
The Alexa+ simulator is partner-only, so the page plays the device and nothing else:
your browser turns speech into text, a language model routes the sentence to one of
the tools this server publishes (the grey line under each sentence is that call), the
real Tally server runs it over an MCP client session, and its own text is read aloud
verbatim while the ui:// balance sheet updates on a mock Echo Show. Details and the
measurements behind the routing choices are in demo/playground.py.
The free instance sleeps after 15 minutes idle; the first request after that takes about a minute.
Related MCP server: CaseChaser for Alexa+
Why voice, and why a shared device
Splitting expenses is not an arithmetic problem — a calculator solved that decades ago. It is a capture problem. Nobody unlocks a phone and opens an app at a restaurant table, so the expense goes unrecorded, and a week later the evening is settled from memory and somebody quietly eats the difference.
Voice removes the capture cost: you say it while the bill is still in your hand.
And Alexa is not a personal device — it sits in the kitchen and belongs to everyone in the apartment. A shared ledger on a shared device is something a per-phone app structurally cannot be: any flatmate can record a purchase or ask where things stand without installing anything.
Being shared is also why the payer gets named out loud. "Sam paid for dinner" works from the kitchen counter whoever is standing at it; "I paid" only means something on an account linked to one person, which is what the invite code establishes.
What it does
Tool | Spoken example |
| "Sam paid 132 for dinner, split with Chris and Maya" |
| "Who owes what?" |
| "Chris paid Sam back 44" |
| "No — cancel that" |
| "Add Dana to the apartment" |
| "Start a household called Apartment 4B" |
| "How much do I owe Chris?" |
| "What did we spend lately?" |
Three decisions that shaped the build
The spoken answer is the product; the screen is a bonus. Every tool returns
a sentence written to be heard once — it leads with what you should do, not
with a table nobody can hold in their head. client_supports_apps() decides
whether a screen exists at all: an Echo Show additionally gets the
MCP Apps
balance sheet, an Echo Dot loses nothing but the picture. Both paths are tested.
Never guess whose money it is. Speech recognition is lossy, so names get resolved exactly, then by unambiguous prefix. "An" with both Anna and Andrew in the apartment does not pick one — it asks, over MCP elicitation, and falls back to putting the question in the error text when the host has no back-channel. Misattributing an expense is worse than one extra question.
Money is integers, end to end. Every amount is a count of minor units, and floats never touch the arithmetic. A three-way split of 10.00 cannot be equal, so the odd cent is handed out on a rotation instead of always landing on the same person. Property-based tests assert the invariants that matter: balances always sum to zero, and the suggested transfers actually clear the ledger.
Architecture
tally/
money.py Minor-unit arithmetic; equal / weighted / exact splits
ledger.py Households, entries, balances, debt simplification
store.py SQLite persistence (WAL)
server.py MCP server: tools, Apps binding, elicitation
ui.py The ui:// app resource for screen devices
auth.py OAuth 2.1 authorization server (PKCE, rotation)
login.py Account linking — redeeming an invite codeThe ledger domain has no MCP dependency and no I/O, which is why its invariants can be hammered with generated input in milliseconds.
Settling up uses the standard greedy max-debtor/max-creditor match. Each
step zeroes at least one person, so it never needs more than n-1 transfers,
against the n(n-1)/2 of paying everybody back individually. (Finding the true
minimum is NP-hard; this is optimal unless a strict subset happens to balance
among itself.)
Running it
uv sync
uv run pytest # 266 tests
uv run python -m tally --stdio # for the MCP InspectorRunning the playground locally
uv run python demo/playground.py # http://127.0.0.1:8978A key in PUBLICAI_API_KEY (free, Public AI) or GEMINI_API_KEY
turns on model routing; without one a pattern router answers and the page marks it.
Seeing it work without an Echo
Alexa+ add-on publishing is limited to partners in the US, so the two scripts in
demo/ stand in for the device. Run all three in separate terminals:
uv run python -m tally --port 8000
uv run python demo/mock_host.py # http://127.0.0.1:8977
uv run python demo/conversation.py --speakconversation.py plays a scripted exchange against the running server: each
line is an utterance, the tool call Alexa+ would route it to, and the server's
own spoken answer — read aloud by the system voice, with per-call latency.
Nothing is stubbed; every reply came over Streamable HTTP.
mock_host.py renders the ui:// app at Echo Show 15 and Echo Show 5 sizes,
performing the same ui/initialize handshake a real host would and logging
every message the app sends back. It follows the live server, so the balance
sheet fills in as the conversation runs.
Speech and screen are rendered differently, on purpose
Alexa reads a tool's text aloud verbatim, so the same amount is written twice:
42 dollars 50 cents in the spoken answer, $42.50 in the structured payload
a screen renders. "42.50 USD" would be read out as "forty two point five zero
U S D".
Over HTTP, the transport Alexa+ uses:
uv run python -m tally --port 8000Connecting it to Alexa+
Alexa needs a public HTTPS origin and OAuth. Expose the local server and pass the public URL — that switch turns on discovery metadata, the authorization code flow with PKCE, and the account-linking page:
cloudflared tunnel --url http://localhost:8000
uv run python -m tally --port 8000 --public-url https://<your-tunnel>.trycloudflare.comAlexa+ does not register itself, so it needs a client configured up front.
Without TALLY_CLIENT_ID and TALLY_REDIRECT_URI (the values from the Alexa
developer console) account linking cannot start at all:
export TALLY_CLIENT_ID=... TALLY_REDIRECT_URI=https://layla.amazon.com/api/skill/link/...Then onboard the add-on:
alexa-ai configure
alexa-ai new mcp --name "Tally" --locale en-US \
--mcp-server-url https://<your-tunnel>.trycloudflare.com/mcp
alexa-ai deployWithout --public-url the server runs unauthenticated — fine for the Inspector
and the test suite, never for a real device.
Joining a household
A name is not a secret. Adding someone creates a placeholder member and a single-use invite code, which the person who set the household up passes on; linking a device redeems that code. Matching on the name instead would let anyone who guessed a flatmate's first name attach their own Alexa to somebody else's ledger.
Protocol conformance
MCP spec 2025-11-25+ over Streamable HTTP.
MCP Apps (
io.modelcontextprotocol/ui): aui://resource served astext/html;profile=mcp-app, bound to four tools via_meta.ui.resourceUri, with aui/initializehandshake and no network access at all in its CSP.OAuth 2.1:
/.well-known/oauth-authorization-server, authorization codePKCE (S256), dynamic client registration, refresh-token rotation, and RFC 8707 resource validation so a token minted for another server is refused.
Tool annotations: read-only and destructive hints, so a host knows what needs confirming before it runs.
Resources and prompts: the ledger is readable at
tally://householdas JSON — reading where things stand is not a side effect and should not need a tool call — and three prompts give a host somewhere to start.Change notifications: tools that move the ledger publish a resource update, so a listening host can refresh instead of going stale. This reaches
subscriptions/listenhosts (2026-07-28); at the 2025-11-25 version Alexa+ negotiates there is no push path at all — see FRICTION.md.Structured output: every ledger tool publishes an
outputSchemaand always returns structured content — what a screen changes is how much has to be said out loud, not what is sent.Latency: ~15 ms median per tool call over HTTP, against the 500 ms Alexa+ allows.
Testing
266 tests, in eight layers:
Property-based (Hypothesis) over the money and ledger invariants — conservation, fairness bounds, settlement correctness.
End-to-end over a real MCP client session — tool schemas, Apps negotiation, structured content, elicitation, error shape, persistence across sessions.
Hostile input — amounts that look numeric but are not, blank and enormous names, nonsense currencies, and shares that cannot be divided. Everything is refused with something a person can act on, and nothing reaches the database it cannot survive.
Concurrency — two dozen simultaneous writers: nothing is lost, the ledger still sums to zero, and no two entries claim the same place in the history.
Two people, one ledger — the claim the product rests on: a second flatmate joins, "I" means a different person for each of them, either can undo a misheard entry, and a stranger reaches nothing.
The playground — amounts said aloud, the fallback router, sandbox isolation between browsers, a slow model raced by a second copy, and the guard rails on what the model may call: only published tools, never a retry key of its own.
OAuth over real HTTP — the whole authorization code flow, plus the attacks it exists to stop: replayed codes and forged PKCE verifiers.
See FRICTION.md for what the Amazon and MCP tooling got right and where it cost time.
Contributing
Where help is useful, and how to run the suite: CONTRIBUTING.md.
License
MIT — see LICENSE.
Available Tools
8 toolsadd_personAdd a personA
Add someone to the household so expenses can be split with them.
Inputs: name (required), also_called. Returns: result
recent_activityRecent activityBRead-onlyIdempotent
List what was recorded lately, most recent first.
Inputs: limit. Returns: result
record_expenseRecord an expenseA
Record that someone paid for something on the household's behalf. This is the main tool: use it the moment the user mentions paying.
Inputs: amount (required), description (required), shares, paid_by, +2 more. Returns: Balances and settlement plan, for speech and screen.
settle_upSettle upB
Record that one person paid another back.
Inputs: to (required), amount (required), paid_by, idempotency_key. Returns: Balances and settlement plan, for speech and screen.
show_balancesWho owes whatARead-onlyIdempotent
Report every member's net position and the fewest payments that settle it.
Inputs: no named parameters. Returns: Balances and settlement plan, for speech and screen.
start_householdStart a householdA
Create a shared ledger and put the speaker in it as the first member.
Inputs: name (required), currency, your_name. Returns: result
undo_lastUndo the last entryADestructive
Reverse the most recent expense or payment, for when something was misheard.
Inputs: idempotency_key. Returns: Balances and settlement plan, for speech and screen.
what_do_i_oweWhat do I oweARead-onlyIdempotent
Answer what the speaker owes one other person, or their overall position when no one is named. Use this for 'how much do I owe Chris'.
Inputs: person. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Each tool has a clearly distinct purpose with no overlap. Record, show, settle, undo, start, add, query, and list actions are uniquely identifiable.
All names use snake_case and mostly follow verb_noun pattern. 'what_do_i_owe' is a phrase rather than a verb_noun, but it remains clear and consistent in style.
With 8 tools, the set is well-scoped for a household expense tracker, covering core actions without excess or deficiency.
Core lifecycle actions are covered, including recording, settling, undoing, household creation, member addition, and queries. Missing explicit edit/delete but undo_last partially compensates.
Maintenance
Related MCP Connectors
Track and split shared expenses across trips, events, and groups. Create groups, add expenses, and…
Split bills from your AI: read bills & balances, create equal splits, request settlements.
Shared ledger for groups that share money: balances, expenses, transfers, budgets and reports.
Track shared expenses, payments and balances across BananaSplit groups and friends.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables users to track expenses by adding, listing, and summarizing them with category support through natural language.-
- AlicenseNot gradedqualityCmaintenanceGives Alexa+ voice access to track household insurance claims, refunds, repairs, and disputes, and to chase them via disclosed AI phone calls while keeping money and legal decisions with the human.MIT
- AlicenseNot gradedqualityCmaintenanceEnables voice-first shared object memory for Alexa+, letting users record where items were last reported, retrieve authorized locations, correct stale records, check shared items in and out, run guided Lost Mode searches, and manage privacy-aware access.MIT
- AlicenseNot gradedqualityCmaintenanceEnables Alexa+ to manage household admin tasks: checking upcoming bills, verifying funding, detecting anomalies, auto-handling routine payments, and escalating only when a human decision is required.1Apache 2.0