# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Receipts → Who Owes Whom
## 1. Project
**Receipts → Who Owes Whom** is the app you open on the flight home
after a weekend with friends, when somebody has paid the Airbnb,
somebody else has paid for groceries, somebody put the boat rental on
their card, and there are forty-seven photos of paper receipts plus
three Apple Pay confirmation emails plus a Splitwise-shaped chaos
sitting in everyone's iMessage thread. Drop the receipts in — the
photos, the email forwards, the screenshots, the group-chat export.
Tap **Settle**. A sandboxed Linux agent spins up, reads each receipt,
classifies it, deduplicates the Apple Pay record against the photo of
the same restaurant bill, applies the split rule you picked (equal,
itemised, by night, by category), runs a minimum-transfer settlement
across the whole group, and ninety seconds later hands back one
table: _Maya owes Sam $73. Sam owes Jordan $44. Alex is square._
You watch the agent work. The progress log streams ("parsing receipt
12 of 47…", "found a likely duplicate of receipt 8 — Apple Pay
confirmation for the same $186.40 at Casa Verde…", "Maya tagged
herself out of the boat rental — excluding her from line item
3 of receipt 31…"). Every line in the final settlement is a tap
away from the source receipt that justifies it. The auditable trail
is the point. Nothing is computed in a black box; the agent's
intermediate notes are visible, and if the group disagrees with how
it allocated the $42 surfboard rental, one tap re-runs the agent
with that line corrected.
The job-to-be-done is the universal post-trip moment. Six people went
to Lake Tahoe. Four people split a Lisbon Airbnb. Eight cousins
shared a wedding-weekend rental in Charleston. A couple split a
restaurant bill three ways with two friends because somebody is
vegetarian and somebody is on a budget. The receipts are everywhere
— the camera roll, three different inboxes, two screenshots in a
group thread, a Venmo request that was later refunded, a PDF
confirmation buried in an Airbnb account. Nobody wants to type forty
line items into Splitwise on a tray table at 30,000 feet.
The single demo that proves the magic: a user uploads a folder of
thirty-eight images and four PDFs from a four-person ski weekend in
Park City. The Managed Agent provisions a sandbox, parses every
receipt (including the two that are partially-blurry, the one that
is in French because the user grabbed the boulangerie receipt while
walking, and the three Apple Pay summary screenshots that overlap
with two of the photos). It deduplicates. It applies the user's
chosen split rule ("equal four ways for shared, itemised for
restaurants where anyone tagged out"). It computes the minimum-
transfer settlement across the four of them — three transfers
instead of six. It exports a Venmo-ready intent payload and a
Splitwise-compatible CSV. The user taps one transfer, Venmo opens
pre-filled with the amount and the memo line ("Park City — split
22 receipts"), and the user reviews and sends. No money moves
without the user's tap. Every settlement line is traceable.
And in the harder cases — eight people across three rental cars in
Iceland, a destination wedding where two couples paid for shared
floral arrangements, a road trip where two people drove the rental
on different days and one person didn't drive at all — the agent
reads the whole arc. It separates the boat rental Maya tagged out
of from the boat rental everyone shared. It catches the Apple Pay
that was a refund of the Airbnb damage deposit and excludes it from
the settlement. It flags the receipt where the OCR confidence was
below 0.7 and asks the user to confirm before settling.
**Tagline:** _Drop the receipts in. A sandboxed agent reads, splits,
and settles them — every line traceable, every transfer your call._
## 2. Target audience
- Friend groups returning from a weekend trip — ski cabin, beach
house, lake rental, music festival, bachelor / bachelorette
weekend, family-reunion rental, college-reunion stay
- Roommate cohorts splitting one-off shared purchases — a new
couch, a stocked-grocery move-in, a shared utility bill that
came in after one tenant moved out
- Travelling work crews who pay personally and reimburse later —
film production assistants on location, touring musicians on the
road, conference attendees who all expensed different lunches
- Wedding parties splitting the cost of a shared rental, the
florist deposit, the rehearsal-dinner restaurant
- Multi-generational families settling a shared holiday rental
where some people paid for groceries, some for excursions, and
the host paid for the Airbnb
- Travel-club organisers who handle one trip a year for eight to
twelve people and need the books closed within a week
- Roommates with a rotating "house card" — whoever is at the
grocery store pays, and the spreadsheet has gotten messy
- Anyone who has ever opened Splitwise on a phone keyboard at the
end of a tiring weekend and decided it could wait until tomorrow
— and then never opened it again
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this app over the
Splitwise + spreadsheet status quo.
- **The agent does the work, not the user.** The hero capability is
the Managed Agents API. The user drops in a pile of receipts and
taps Settle. A sandboxed Linux agent provisions, reads each
receipt with multimodal Gemini, classifies, deduplicates,
allocates, and computes the minimum-transfer settlement.
Ninety seconds, not ninety minutes.
- **Every line is traceable.** Every dollar in the final
settlement table links back to the receipt that contributed it
and the agent's decision about how to split it. Tap a transfer
amount → see the contributing receipts, the per-person
allocation, and the agent's note about why. Auditability is the
point. Nothing is opaque about money.
- **The agent's steps are visible while it works.** A streaming
progress log shows what the agent is parsing right now, what it
flagged as a duplicate, what it set aside as low-confidence and
is asking the user to confirm. Watching the work happen builds
trust. Black-box settlement is unacceptable when real money
changes hands.
- **No money moves without explicit tap-through.** The agent
computes the settlement and prepares Venmo / Cash App / Wise /
bank-transfer payloads, but every transfer requires the user to
open the app and confirm. The Build app never holds funds, never
authorises transfers, never stores payment credentials. The
closing tap is always the user's.
- **Multiple split rules, swappable per receipt.** Equal split,
itemised (per-line, with per-person tag-outs), by night (for
rentals where some people arrived late), by category (groceries
split equally, restaurant split itemised), or custom percentage.
The user picks the rule once at trip level, overrides per
receipt if needed. The agent applies whichever rule it sees
attached to each receipt.
- **Deduplicates Apple Pay + photo + confirmation email.** A
$186.40 dinner at Casa Verde might appear three times: as a
photo of the paper receipt, as an Apple Pay confirmation
screenshot, and as an email from the restaurant's reservation
system. The agent matches them by amount, merchant, date, and
time-window and presents the deduplicated single line — but
preserves all three sources so the user can audit.
- **Exports to the tools the group already uses.** A Venmo /
Cash App / Wise / Revolut intent payload for the
recipient + amount + memo. A Splitwise-compatible CSV for users
who want to import into Splitwise for record-keeping. A
per-person PDF summary that can be sent to anyone in the group
who wasn't part of the upload.
- **Receipts stay private to the group.** Photos and PDFs are
uploaded to the group's private Firebase Storage bucket, only
visible to invited members of the trip. The Managed Agent's
sandbox is ephemeral — state is destroyed after the settlement
is computed. No receipt content is sent to Gemini for model
training.
- **The hard rule: it's a settlement calculator, not a payment
processor.** Every screen that references money flowing makes
this explicit. The user reviews, the user pays. The app
computes; the user authorises.
## 4. Features to build
- One-tap **Settle** button on the trip page — kicks off the
Managed Agents API run with the current pile of receipts and
the trip's chosen split rule
- Receipt drop zone (mobile-first) — camera capture, camera-roll
multi-select, file upload (PDF, JPEG, PNG, HEIC), drag-and-drop
on desktop; supports up to 100 receipts per trip
- Email-forwarding inbox — each trip gets a unique forwarding
address (e.g. `parkcity-2026-may@in.receipts.app`); users forward
Airbnb confirmations, restaurant receipts, ride-share emails
directly into the trip's pile
- Apple Pay / Google Pay screenshot ingestion — the agent
recognises the iOS/Android transaction-screenshot UI and parses
the merchant, amount, date, last-four
- Group chat import — paste an iMessage / WhatsApp / Telegram
export and the agent extracts mentions of payments ("I just
paid for the cabin, $1,240"), tagging them as **unverified
claims** until matched with a real receipt
- **Streaming agent progress log** — visible while the Managed
Agent runs; shows parsing, classification, deduplication, and
allocation steps in real time
- **Auditable settlement table** — final output: a matrix of
who-owes-whom, minimum-transfer optimised; every cell tappable
to drill into the contributing receipts
- Per-receipt review pane — view the photo + the parsed line
items + the per-person allocation; edit anything, re-run the
settlement
- Split-rule picker at trip level: equal / itemised / by night /
by category / custom percentage
- Per-receipt rule override — for the one receipt where Sam
ordered three drinks and everyone else had one, switch this
receipt to itemised even if the trip default is equal
- Member roster — invite trip members via magic link; each member
sees their personal owe / owed summary
- Per-receipt tag-outs — Maya didn't go on the boat ride, so she
is excluded from receipt 31; one tap per person per receipt
- Currency handling — multi-currency trips (the Lisbon weekend
paid some receipts in EUR, some in USD); FX conversion via
daily rates the agent fetches with search grounding; final
settlement in the trip's primary currency
- Tip-and-tax allocation — when itemising a restaurant receipt,
the agent allocates tip and tax proportionally to each line
- Refund detection — the agent recognises the negative amount on
the Apple Pay damage-deposit refund and excludes it (or includes
it as a credit, depending on user preference)
- Venmo / Cash App / Wise / Revolut deep-link generation — each
transfer in the settlement has a one-tap intent button that
opens the user's chosen payment app pre-filled
- Splitwise CSV export — for users who want to round-trip into
Splitwise for long-term record-keeping
- Per-trip PDF summary — a clean PDF with the settlement table,
the deduplicated receipt list, and the per-person breakdown
- Trip archive — past trips kept indefinitely with the original
receipts preserved; useful when somebody messages six months
later asking "wait, did we ever settle the Vegas weekend?"
- Receipt re-OCR — if the user disagrees with the agent's parse
of a receipt, one tap re-runs the parse on that single receipt
with a higher-thinking prompt
- Disagreement / dispute thread — if Maya disputes the agent's
allocation of the $42 surfboard rental, she can flag it; the
agent re-runs with her objection in the input and proposes a
revised allocation
- Offline mode — receipts captured offline upload when connection
returns; nothing is computed locally because settlement
requires the agent
- Empty-state illustrations — Nano Banana Pro generated; depict
a stack of receipts on a kitchen table, a phone showing a
settlement table, no people
- Privacy controls — every trip member can request export and
deletion of their data at any time, per GDPR
- Settlement re-run — if anyone uploads a receipt after the
initial settle, one tap re-runs the agent on the full pile
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities
below. Wire them up explicitly — the Managed Agents API is the
load-bearing hero capability and the reason this template exists
post-I/O 2026.**
### Gemini capabilities (the load-bearing intelligence)
- **Managed Agents API** (`antigravity-preview-05-2026`) — the
hero capability. One call provisions a sandboxed Linux agent
that lives for the duration of a settlement run (typically
60–180 seconds). The agent's behaviour is defined by an
`AGENTS.md` + per-skill `SKILL.md` set dropped into its sandbox
at provisioning time. Skills used in this template: `parse-
receipt`, `dedupe-receipts`, `allocate-line-items`, `compute-
settlement`, `format-export`. The agent has code-execution +
file-management + web-browsing tools inside the sandbox; it
uses code-exec for the minimum-transfer optimisation and for
FX-rate fetching, file-management for working with the
uploaded receipt files, and web-browsing only when grounding
is needed (e.g. currency rates for the trip dates). **Preview
status** — flag in the capabilities-info panel; no SLA.
*Note: the Managed Agents API is in public preview as of
2026-05-19 and the exact request payload + `AGENTS.md`/`SKILL.md`
schema were not pinned in public documentation at I/O 2026 —
verify the SDK call structure against the live Interactions API
reference before shipping, and keep the sequential-3.5-Flash
fallback path described in section 6 ready for users who can't
reach the preview yet.*
- **Multimodal receipt parsing** (`gemini-3.5-flash`) — inside
the agent's sandbox, the `parse-receipt` skill calls Gemini
3.5 Flash on each receipt image / PDF with `responseSchema`
set to a `ParsedReceipt` JSON schema. 3.5 Flash beats prior-
generation Pro on multimodal benchmarks (CharXiv 84.2%) at 4x
the speed and a fraction of the cost — the right default for
high-volume receipt parsing.
- **Structured output / JSON Schema** — every Gemini call in
this template returns structured JSON via `responseSchema`.
The schemas are seeded verbatim in the system instruction. The
Zod schemas below are converted to Gemini's `Schema` type via
the SDK helper before being passed as `responseSchema`. Numeric
constraints (e.g. confidence 0..1) are documentation in the
schema and clamped on the server after the response arrives.
- **Currency rate grounding** (`gemini-3.5-flash` + `google_
search`) — when the trip spans multiple currencies, the agent
fetches daily FX rates for the receipt dates via grounded
search. **Critical**: grounded calls cannot combine
`responseSchema` with `google_search` — the model emits JSON
in the text body, and citation URLs come from
`response.groundingMetadata.groundingChunks[].web.uri`. The
agent's `fetch-fx-rate` skill handles this.
- **Inline image preview generation** (`gemini-3.1-flash-image`,
Nano Banana 2) — empty-state illustrations and the hero
landing illustration (a stack of receipts becoming a tidy
settlement table). Used at app design time + as the empty-
state for a trip with no receipts yet.
- **Nano Banana Pro for the settlement summary card**
(`gemini-3-pro-image`) — the per-trip PDF summary includes a
designed cover with the trip name + dates + member avatars,
rendered with legible 4K typography by Nano Banana Pro. This
is the Pro variant's killer feature post-I/O 2026: real
typography in-image. The cover is generated once per
finalised settlement and cached.
- **Thinking levels** — `medium` for the agent's overall run
(multi-step reasoning across many receipts). `low` for the
per-receipt parse call (the receipt structure is bounded and
doesn't benefit from deeper reasoning). `medium` for the
allocation skill (which needs to reason about tag-outs and
itemisation). No `high` thinking in this template — the work
is structured, not exploratory.
- **TTS for accessibility readback** (`gemini-3.1-flash-tts-
preview`) — optional. Reads the settlement aloud for users who
prefer audio confirmation before tapping through to Venmo.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-
provisioned by AI Studio Build). **Apple sign-in is optional**
and requires an Apple Developer account, Service ID, Key ID,
and private key wired into the Firebase Auth console. **Magic-
link email** is used heavily — trip members are invited via
magic link, not asked to create accounts upfront. The sender
domain must be authorised in Firebase Auth before first invite.
- **Database — Required.** Firestore for `users`, `trips`,
`trip_members`, `receipts`, `parsed_line_items`, `settlements`,
`settlement_transfers`, `agent_runs`, `disputes`. Receipts and
settlements are scoped to trips; only invited members can read.
- **File storage — Required.** Firebase Storage for receipt
images, PDFs, screenshots, and the generated PDF summary
covers. **Firebase Storage is NOT auto-provisioned by AI Studio
Build today** — enable it in the Firebase console and wire the
bucket name into the AIS Build project before the first
receipt is uploaded. Receipt files are private by default; only
trip members get pre-signed URLs.
- **Cloud Run server functions — Required.** Two functions:
`runSettlement` (invokes the Managed Agents API and streams the
agent's progress events back to the client over SSE) and
`finaliseSettlement` (persists the agent's final output to
Firestore + Storage). Both functions hold the Gemini API key
in Secrets Manager.
- **Email — Required (transactional).** Magic-link invitations
to trip members and the post-settlement summary email. Sender
domain must be authorised in Firebase Auth.
- **Email-forwarding inbox — Required.** Each trip gets a unique
forwarding address. Implemented via a SendGrid Inbound Parse
webhook (or AWS SES + Lambda equivalent); the webhook posts
the email body + attachments to the trip's receipt bucket.
- **Payments — Not in v1.** This is a settlement calculator,
not a payment processor. The app generates payment intents
(Venmo `venmo://` URL scheme, Cash App `cash.me/` link, Wise
/ Revolut deep links) but never moves money. No Stripe
integration in v1.
- **External APIs:** Gemini API for all intelligence. No other
required external API.
**Environment variables:** every secret (Gemini API key, Firebase
service account JSON, SendGrid webhook secret) lives in environment
variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store
passwords in plain text · use HTTPS everywhere · honour 'delete my
account' inside the UI · explicit opt-in for any analytics · receipt
files and parsed line items are never sent to Gemini for model
training (use the Gemini API on the paid tier, where Google does
not use your content for model training, per the Gemini API
Additional Terms) · the Managed Agent's sandbox is ephemeral, and
the receipts are passed in as private Files API URIs scoped to the
agent's session — destroyed when the session ends.
**Read this first — prompt-craft rules that apply to every call in
this template:**
1. **Name the model variant explicitly** in every Gemini API call
AND in every `SKILL.md` definition inside the agent's sandbox.
The agent picks tools; the agent does NOT pick models — every
sub-call inside the agent specifies its model. See the per-call
matrix below.
2. **Pin `thinkingLevel` explicitly** per call. The agent's
overall `Interactions.run` call uses `medium`; the sub-calls
the agent's skills make have their own pinned values. See the
matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in
the system instruction or `responseSchema` field. The schemas
are literal below. **Convert the Zod schema to Gemini's
`Schema` type via the SDK helper** before passing as
`responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max`
constraints in `responseSchema` are documentation only; clamp
server-side after the response arrives.**
4. **Pin the system instruction separately** from user input. Use
the `systemInstruction` field for persona + behavioural rules;
use `contents` for the actual receipt files / user query.
5. **Pre-declare tools as an enable/disable list** per call. The
matrix below names which tools each call enables. The Managed
Agent's overall run enables `code_execution` + `file_
management` + `web_browsing`; the sub-calls the agent's
skills make enable their own narrower tool sets.
6. **State negative constraints explicitly** — listed below. They
are hard rules, not "be careful" suggestions.
7. **The Managed Agents API is preview.** Flag it in the
capabilities-info panel: "preview, no SLA, may change". Build
a fallback path that uses sequential Gemini 3.5 Flash calls
(without the agent sandbox) for users who hit a preview-quota
error; the UX downgrades gracefully but the result is the
same.
8. **The model extracts entities; ALL split math, currency
rounding, and minimum-transfer settlement happens in
server-side code.** Specifically: `parse-receipt` extracts
line items, owner tags, and integer cents from images;
`compute-settlement` is deterministic TypeScript inside the
agent sandbox (graph-reduction over signed balances, integer
cents only, no floats). The model never computes splits,
never sums totals, never decides who owes whom.
9. **Files API uses `files/*` resource names, not `gs://` URIs.**
The AI Studio Build runtime uses the Gemini Developer API
(`@google/genai` SDK). Files API `upload` returns a resource
name of the form `files/abc123xyz`, passed via `fileData:
{ fileUri, mimeType }`. `gs://` URIs belong to Vertex AI /
Cloud Storage — a different surface, not accepted here.
10. **Grounded responses can wrap JSON in ```json fences or add
prose preamble** (the `fetch-fx-rate` skill uses
`google_search`). Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
11. **Strip unsupported Zod modifiers before passing to
`responseSchema`** — Gemini's OpenAPI subset rejects `.regex()`
/ `pattern`, fixed-length `z.tuple()`, and other custom
validators. Use a sanitizer that flattens tuples to arrays and
removes regex patterns before serializing. Validate those
constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Settlement run (the agent overall) — `runSettlement` | `antigravity-preview-05-2026` | medium | `code_execution`, `file_management`, `web_browsing` (the agent's sandbox tools) |
| `parse-receipt` skill — image / PDF → `ParsedReceipt` | `gemini-3.5-flash` | low | (none) |
| `dedupe-receipts` skill — list of `ParsedReceipt` → `DedupedReceiptSet` | `gemini-3.5-flash` | medium | (none) |
| `allocate-line-items` skill — `DedupedReceiptSet` + `TripRules` → `AllocatedReceiptSet` | `gemini-3.5-flash` | medium | (none) |
| `fetch-fx-rate` skill (only when trip is multi-currency) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| `compute-settlement` skill — done in code (TypeScript / Python inside the sandbox), NOT via a Gemini call | n/a | n/a | n/a |
| `format-export` skill — `Settlement` → Venmo intents / Splitwise CSV / PDF data | `gemini-3.5-flash` | low | (none) |
| Per-trip PDF summary cover illustration | `gemini-3-pro-image` (Nano Banana Pro) | n/a | n/a |
| Hero / empty-state illustration generation | `gemini-3.1-flash-image` (Nano Banana 2) | n/a | n/a |
| TTS readback of the settlement (optional) | `gemini-3.1-flash-tts-preview` | n/a | n/a |
*Note for builders:* on TTS and image-generation calls, omit
`thinkingConfig` entirely — the field is not supported on those
models. The `n/a` cells in this matrix are documentation only; do
not serialise them into the request body. Grounded search calls
emit JSON in the text body — `responseSchema` and `google_search`
cannot be combined in the same Gemini call; parse the JSON
server-side and read citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri`. The
**`compute-settlement`** skill is **deliberately deterministic
code, not a Gemini call** — minimum-transfer settlement is a
graph-reduction problem the model would solve incorrectly more
often than it would solve correctly. The agent's sandbox runs the
algorithm in Python (NetworkX or hand-rolled). The agent only
calls Gemini for parsing, deduplication, allocation, and export
formatting.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
const Currency = z.enum([
"usd", "eur", "gbp", "cad", "aud", "nzd",
"jpy", "krw", "cny", "hkd", "sgd", "thb", "inr",
"mxn", "brl", "ars", "clp", "cop",
"chf", "sek", "nok", "dkk", "isk",
"pln", "czk", "huf", "ron",
"zar", "egp", "aed", "sar",
"other",
]);
const Money = z.object({
amount_minor_units: z.number(), // integer cents / yen
currency: Currency,
currency_other_iso: z.string().nullable(), // when currency: "other"
});
const ReceiptSource = z.enum([
"photo_camera",
"photo_upload",
"pdf_email_forward",
"pdf_upload",
"apple_pay_screenshot",
"google_pay_screenshot",
"venmo_screenshot",
"cash_app_screenshot",
"email_body_text",
"manual_entry",
"group_chat_export_claim",
"other",
]);
const LineItem = z.object({
line_id: z.string(),
description: z.string(), // verbatim from receipt
description_normalised: z.string().nullable(), // model-normalised
quantity: z.number().nullable(),
unit_price: Money.nullable(),
total: Money,
category: z.enum([
"food_dine_in", "food_takeout", "food_groceries",
"alcohol", "coffee",
"lodging_per_night", "lodging_one_time", "lodging_deposit",
"transport_rideshare", "transport_taxi",
"transport_public_transit", "transport_rental_car",
"transport_fuel", "transport_parking",
"transport_flight", "transport_train",
"activity_admission", "activity_rental",
"activity_tour", "activity_excursion",
"shopping_souvenir", "shopping_household", "shopping_other",
"utilities", "fees_tax", "fees_tip", "fees_service",
"refund", "other",
]),
is_tip_or_tax: z.boolean(),
per_person_allocation: z.array(z.object({
member_id: z.string(),
share_amount_minor_units: z.number(),
})).nullable(), // null until allocate step runs
});
const ParsedReceipt = z.object({
receipt_id: z.string(),
trip_id: z.string(),
source: ReceiptSource,
source_file_uri: z.string(), // Files API `files/*` resource name (Developer API)
uploaded_by_member_id: z.string(),
uploaded_at_iso: z.string(),
merchant_name_verbatim: z.string().nullable(),
merchant_name_normalised: z.string().nullable(),
merchant_city: z.string().nullable(),
merchant_country_iso: z.string().nullable(),
transaction_date_iso: z.string().nullable(), // YYYY-MM-DD if present
transaction_time_iso: z.string().nullable(), // HH:MM if present
last_four: z.string().nullable(), // card last 4, if visible
subtotal: Money.nullable(),
tip: Money.nullable(),
tax: Money.nullable(),
total: Money, // the canonical receipt total
line_items: z.array(LineItem),
paid_by_member_id: z.string().nullable(), // who paid; null until tagged
is_refund: z.boolean(), // negative-amount detection
refund_of_receipt_id: z.string().nullable(),
ocr_confidence: z.number().min(0).max(1),
parse_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
food_safety_flag: z.boolean(), // always false for this template; reserved
});
const DuplicateGroup = z.object({
group_id: z.string(),
receipt_ids: z.array(z.string()), // 2+ receipts the agent considers the same transaction
match_confidence: z.number().min(0).max(1),
match_basis: z.array(z.enum([
"merchant", "amount", "date", "time_window",
"last_four", "currency",
])),
canonical_receipt_id: z.string(), // the one kept; others marked duplicates
});
const DedupedReceiptSet = z.object({
trip_id: z.string(),
unique_receipts: z.array(z.string()), // canonical receipt_ids
duplicate_groups: z.array(DuplicateGroup),
excluded_receipts: z.array(z.object({
receipt_id: z.string(),
reason: z.string(), // "refund of receipt X", "duplicate of Y"
})),
});
const TripMember = z.object({
member_id: z.string(),
display_name: z.string(),
email_or_phone_hash: z.string(), // hashed contact for invite
});
const SplitRule = z.enum([
"equal",
"itemised",
"by_night",
"by_category",
"custom_percentage",
]);
const TripRules = z.object({
trip_id: z.string(),
members: z.array(TripMember),
default_rule: SplitRule,
primary_currency: Currency,
per_receipt_overrides: z.array(z.object({
receipt_id: z.string(),
rule: SplitRule,
})),
per_receipt_tag_outs: z.array(z.object({
receipt_id: z.string(),
member_ids_excluded: z.array(z.string()),
})),
per_line_tag_outs: z.array(z.object({
receipt_id: z.string(),
line_id: z.string(),
member_ids_excluded: z.array(z.string()),
})),
custom_percentages: z.array(z.object({
receipt_id: z.string(),
percentages: z.array(z.object({
member_id: z.string(),
percent: z.number(), // 0..100
})),
})),
nights_per_member: z.array(z.object({
member_id: z.string(),
nights: z.number(),
})).nullable(),
});
const AllocatedReceiptSet = z.object({
trip_id: z.string(),
allocated_receipts: z.array(ParsedReceipt), // line_items now have per_person_allocation
unresolved_allocations: z.array(z.object({
receipt_id: z.string(),
line_id: z.string().nullable(),
reason: z.string(),
})),
});
const Transfer = z.object({
transfer_id: z.string(),
from_member_id: z.string(),
to_member_id: z.string(),
amount: Money,
contributing_receipt_ids: z.array(z.string()),
contributing_line_ids: z.array(z.string()),
agent_rationale: z.string(), // 1-2 sentences linking transfer to receipts
});
const Settlement = z.object({
settlement_id: z.string(),
trip_id: z.string(),
computed_at_iso: z.string(),
agent_run_id: z.string(), // links to the Managed Agent's session
primary_currency: Currency,
per_member_paid: z.array(z.object({
member_id: z.string(),
paid_total: Money,
})),
per_member_owed: z.array(z.object({
member_id: z.string(),
owed_share: Money,
})),
per_member_balance: z.array(z.object({
member_id: z.string(),
balance: Money, // negative = owes; positive = owed
})),
transfers: z.array(Transfer), // minimum-transfer optimised
number_of_transfers: z.number(),
naive_transfer_count: z.number(), // for comparison ("would have been 6, agent reduced to 3")
warnings: z.array(z.string()), // e.g. "Receipt 14 OCR confidence 0.62 — please confirm"
fx_rates_used: z.array(z.object({
from_currency: Currency,
to_currency: Currency,
rate: z.number(),
rate_date_iso: z.string(),
source_url: z.string().nullable(),
})),
});
const ExportArtifact = z.object({
trip_id: z.string(),
settlement_id: z.string(),
venmo_intents: z.array(z.object({
transfer_id: z.string(),
venmo_deep_link: z.string(),
pre_filled_memo: z.string(),
})),
cashapp_intents: z.array(z.object({
transfer_id: z.string(),
cashapp_deep_link: z.string(),
pre_filled_memo: z.string(),
})),
wise_intents: z.array(z.object({
transfer_id: z.string(),
wise_deep_link: z.string(),
pre_filled_memo: z.string(),
})),
splitwise_csv_text: z.string(),
pdf_summary_storage_path: z.string(), // Firebase Storage path (internal; signed URLs surface to clients)
});
type ParsedReceipt = z.infer;
type DedupedReceiptSet = z.infer;
type AllocatedReceiptSet = z.infer;
type Settlement = z.infer;
type ExportArtifact = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent fabricates a duplicate match — e.g. claims two receipts
with the same total are duplicates when one was a $42 Uber and
the other was a $42 grocery run. The `dedupe-receipts` system
instruction must require **at least two of**: merchant match,
date within 24h, last-four match. The server validates that the
match_basis array has ≥2 entries; if not, reject and re-request.
- Agent computes the settlement itself instead of using the
sandbox's code-execution tool. Minimum-transfer settlement is a
graph reduction; an LLM gets the arithmetic wrong with high
probability. The `AGENTS.md` MUST instruct the agent to run the
algorithm as Python in the sandbox, not in its own reasoning.
The server validates that the Settlement has an `agent_run_id`
and that the `compute-settlement` step in the run logs shows a
code-execution event, not a Gemini-text inference.
- OCR confidence below 0.7 silently used — the `parse-receipt`
call's `ocr_confidence` field must be respected; receipts under
0.7 are flagged in the UI and must be confirmed by a member
before being included in the settlement. The agent's progress
log shows the count of low-confidence receipts.
- Refund detection wrong direction — an Apple Pay damage-deposit
refund of $-500 might be parsed as a $500 charge if the
negative sign is missed. The `parse-receipt` system instruction
must require explicit `is_refund` detection from the visible
sign or the screen UI (Apple Pay shows refunds in a different
color and with a return arrow icon). When unsure, set
`is_refund` to false and flag for review.
- FX rates pulled from training data instead of grounded — the
`fetch-fx-rate` skill MUST use `google_search` grounding for
the rate as of the receipt's transaction date. Server-side
caches the rate per (from, to, date) so a 38-receipt trip
doesn't make 38 grounded calls.
- Tip / tax double-counted — if a receipt's `subtotal` + `tip`
+ `tax` doesn't equal `total` within $0.02 the agent flags it.
The most common bug is the model including tip both as a line
item and as the `tip` field, which inflates the total. The
schema requires `is_tip_or_tax: true` on tip / tax line items
so they're handled correctly during allocation.
- Per-line tag-out applied to wrong line — when itemising a
restaurant bill, the agent must match each `LineItem.line_id`
to the `per_line_tag_outs` exactly. Off-by-one is the typical
failure; the system instruction shows worked examples and the
schema requires `line_id` to be a stable string per receipt.
- Trip member identification — when a receipt's `paid_by_member_
id` is ambiguous (multiple members of the trip have last-four
ending in 1234), the agent must NOT guess. Set
`paid_by_member_id: null` and flag for user review with the
candidates listed.
- Agent's progress log leaks PII — receipt content might contain
the last four of a card, an order number, a member's email
forwarded into the inbox. The progress-log system instruction
must require the agent to redact card numbers, email addresses,
and phone numbers from the streamed progress events. The full
detail is stored only in the trip's private database.
- Apple Pay screenshot conflated with the underlying receipt —
the user took a photo of the paper receipt at the restaurant
AND has the Apple Pay confirmation in their email. The agent
should detect both, mark them as the same transaction, and
pick the paper receipt as canonical (it has the itemised line
items, the Apple Pay confirmation only has the total).
- Group-chat-import claims promoted to receipts without
confirmation — if a user pastes "Sam paid for groceries, $200"
from iMessage, the agent records it as an **unverified claim**
(source: `group_chat_export_claim`), NOT as a receipt. The
user must attach a receipt or mark the claim as accepted
before it's included in the settlement.
- Long-context overflow on a 100-receipt trip — receipts at
~2,000 tokens each (image + parsed JSON) means a 100-receipt
trip is ~200k tokens for the agent's working memory.
Comfortably within Gemini 3.5 Flash's 1M context, but the
agent should chunk by sub-task: parse all receipts first
(each in its own call), then dedupe (passing only the parsed
JSON, not the images), then allocate. Never pass all 100
raw images to a single Gemini call.
### Negative constraints (hard rules)
- Do NOT move money. The app generates payment intents and the
user authorises every transfer. No Stripe, no Plaid, no bank
credentials stored. The settlement is computed; the user pays.
- Do NOT compute the minimum-transfer settlement in a Gemini
call. The arithmetic is done in code inside the agent's
sandbox. The Gemini calls handle parsing, deduplication,
allocation, and export formatting.
- Do NOT invent receipts, merchants, amounts, or transactions
that were not in the uploaded files. The agent's progress log
cites the receipt_id for every line in the settlement; the
server validates that every contributing_receipt_id in every
Transfer is a real receipt in the trip's bucket.
- Do NOT promote group-chat claims to receipts without explicit
user confirmation. A claim is a claim; only an attached
receipt promotes it to a confirmed line in the settlement.
- Do NOT silently include receipts with `ocr_confidence < 0.7`
in the settlement. The user must confirm them first.
- Do NOT mix currencies in the final settlement without an FX
rate sourced via grounded search. Every fx_rate_used has a
source_url; the user can audit the rate.
- Do NOT translate merchant names, line-item descriptions, or
category names. The original language is preserved in
`_verbatim` fields; the normalised version is a separate field.
- Do NOT share a trip's receipts beyond the invited members.
Magic-link invitations are revocable; revoking a link removes
the member's access immediately.
- Do NOT use receipt content for model training. Use the Gemini
API on the paid tier, where Google does not use your content
for model training, per the Gemini API Additional Terms. The
capabilities-info panel says this in plain English.
- Do NOT keep the Managed Agent's sandbox alive after the
settlement is computed. The sandbox is ephemeral; teardown
happens automatically when the `Interactions.run` call
returns. The progress-log events are streamed to the client
and persisted; the sandbox's file system is destroyed.
- Do NOT prompt the user to settle before they've reviewed
flagged receipts. The Settle button is disabled until the
flagged count is zero.
- Do NOT compute a settlement when the trip has fewer than 2
members or zero receipts. Show a friendly empty state instead.
- Do NOT alter past settlements silently. If a member uploads a
new receipt after a settlement is finalised, the app prompts
to re-run; the new settlement is saved alongside the previous
one, not replacing it.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini
API call the built app makes (including the calls the Managed Agent
makes from inside its sandbox via the `Skill` invocations defined in
`AGENTS.md`).
### Call: Settlement run (the Managed Agent overall)
Model: `antigravity-preview-05-2026` · thinkingLevel: medium · Tools: code_execution + file_management + web_browsing (sandbox tools)
```
You are a settlement agent. You receive a folder of receipt files
(images, PDFs, screenshots) and a TripRules object that names the
trip's members, the primary currency, the default split rule, and
any per-receipt overrides or tag-outs. Your job is to produce a
final Settlement that pays every member back fairly, with the
minimum number of transfers, and with every transfer traceable to
the receipts that justify it.
You run inside an ephemeral Linux sandbox. The receipt files are
mounted at /workspace/receipts/*. The TripRules JSON is at
/workspace/trip_rules.json. Your scratchpad for intermediate
results is /workspace/scratch/. Your final output must be written
to /workspace/output/settlement.json matching the Settlement
schema and /workspace/output/export.json matching the
ExportArtifact schema.
You have five skills available (defined in
.agents/skills/*/SKILL.md):
- parse-receipt: calls Gemini 3.5 Flash to parse one receipt
file into a ParsedReceipt
- dedupe-receipts: calls Gemini 3.5 Flash to identify duplicate
transactions across the parsed receipts
- allocate-line-items: calls Gemini 3.5 Flash to apply the
TripRules to each receipt's line items
- fetch-fx-rate: calls Gemini 3.5 Flash with google_search
grounding to fetch the FX rate for a specific currency pair
and date
- compute-settlement: runs Python code in the sandbox to compute
the minimum-transfer settlement (you do NOT compute this in
natural language reasoning — you ALWAYS invoke this skill,
which runs the algorithm in code)
- format-export: calls Gemini 3.5 Flash to format the final
Settlement into Venmo / Cash App / Wise intents + Splitwise
CSV text
Required execution order:
1. List the files in /workspace/receipts/ and invoke parse-receipt
on each, writing the result to /workspace/scratch/parsed/.json
2. Invoke dedupe-receipts on the full set of parsed receipts.
Write the result to /workspace/scratch/deduped.json.
3. If the receipts span multiple currencies, invoke fetch-fx-rate
for each (from, to, date) tuple needed. Cache the rates in
/workspace/scratch/fx.json.
4. Invoke allocate-line-items with the deduped set + TripRules +
FX rates. Write the result to
/workspace/scratch/allocated.json.
5. Invoke compute-settlement with the allocated set. Read its
output Settlement JSON from /workspace/scratch/settlement.json.
6. Invoke format-export with the Settlement to produce the
ExportArtifact. Read its output from
/workspace/scratch/export.json.
7. Copy /workspace/scratch/settlement.json and export.json into
/workspace/output/.
Streaming progress events:
- Emit one progress event per skill invocation, with a short
human-readable status line ("parsing receipt 12 of 47…",
"found a likely duplicate of receipt 8…", "computing
minimum-transfer settlement…"). These events are streamed
to the user in real time.
- REDACT card numbers, email addresses, and phone numbers from
every progress event. The detail can live in the persisted
database; the streaming events visible to the user must be
PII-clean.
- If a receipt is flagged for user review (ocr_confidence < 0.7
or ambiguous paid_by_member_id), emit an event naming the
receipt and the reason, and KEEP it in the deduped set so
the user sees it in the final review pane.
Hard rules:
- You MUST invoke the compute-settlement skill via code
execution. You MUST NOT compute the minimum-transfer
settlement in your own natural-language reasoning. The
algorithm is a graph reduction; you will get it wrong if you
try to reason through it. ALWAYS run the Python.
- You MUST cite contributing_receipt_ids on every Transfer in
the output Settlement. Every contributing_receipt_id MUST be
a real receipt_id from /workspace/scratch/parsed/. The
server validates this.
- You MUST NOT alter the contents of the receipt files
themselves. They are read-only.
- You MUST NOT make network calls outside of the web_browsing
tool's grounded-search usage in fetch-fx-rate. No external
APIs.
- You MUST emit a final progress event with status "done" once
/workspace/output/settlement.json exists and validates
against the Settlement schema.
- If any skill fails, emit an event with the failure reason and
stop. Do not retry silently.
- The sandbox is ephemeral. Do not store anything you want to
keep outside /workspace/output/.
Operate only on the files you were given. Emit JSON outputs only.
```
---
### Call: parse-receipt skill — image / PDF → ParsedReceipt
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You parse one receipt into a ParsedReceipt JSON object. The
receipt is provided as a Gemini Developer API Files API
`files/*` resource name (image or PDF), passed via
`fileData: { fileUri: "files/abc123xyz", mimeType }`, with a
short context header naming the source type (photo_camera,
photo_upload, pdf_email_forward, apple_pay_screenshot, etc.).
Read the receipt carefully. Capture:
- merchant_name_verbatim — exactly as printed on the receipt
- merchant_name_normalised — a canonical version ("McDonald's
#4827" → "McDonald's") for matching across duplicates
- merchant_city, merchant_country_iso — from the receipt
address if present
- transaction_date_iso (YYYY-MM-DD), transaction_time_iso
(HH:MM)
- last_four — last four digits of the card if visible
- subtotal, tip, tax, total — each as Money with amount in
minor units (cents for USD/EUR, yen for JPY)
- line_items — every itemised line on the receipt, with
description, quantity, unit_price, total, category
- is_tip_or_tax: true on the tip and tax line items
- paid_by_member_id: null (you do not know who paid; the
agent's allocate step will tag this from TripRules)
For source: apple_pay_screenshot / google_pay_screenshot /
venmo_screenshot / cash_app_screenshot — these are not paper
receipts; they show ONLY the total + merchant + date. line_items
will be a single line with the merchant name and the total.
ocr_confidence reflects how clearly the screenshot text reads.
For source: email_body_text — text-only, no image. Parse the
visible amount, merchant, date from the email body. Do NOT
follow links inside the email.
For source: group_chat_export_claim — a text claim like "Sam
paid for groceries $200". Treat as a claim, not a verified
receipt. ocr_confidence: 0.5 maximum. flagged_for_user_review
must include a flag noting that this is a claim without an
attached receipt.
Refund detection (is_refund):
- Apple Pay / Google Pay refunds show in a different color
(typically green) and have a return arrow icon. Read the
amount as positive but set is_refund: true.
- Paper receipts with negative line items (e.g. "-$500.00
damage deposit refund") set is_refund: true.
- When uncertain, is_refund: false and flag for review.
OCR confidence:
- 0.9+ : crisp, machine-printed, every digit clear
- 0.7-0.9 : mostly clear with one or two ambiguous digits
- 0.5-0.7 : significant blur, faded thermal paper, partial
occlusion
- < 0.5 : largely illegible; flag aggressively
Hard rules:
- Money amounts in minor units (integer cents). 12.50 USD is
1250, not 12.50.
- Currency from the receipt explicitly — currency symbol, ISO
code on the receipt header, country domain on the merchant
email, or the Apple Pay region. When ambiguous, set to
"other" and flag for review.
- Do NOT translate merchant names, line-item descriptions, or
category labels. The _verbatim fields preserve the original
language; the _normalised fields are an English / canonical
representation.
- Do NOT invent line items. If a receipt shows a single
"Total" with no itemisation, line_items is a single line
with that total.
- Do NOT guess paid_by_member_id. Set to null.
- food_safety_flag is always false for this template; reserved.
- flagged_for_user_review names every field with confidence
below 0.7 and a one-sentence reason.
Output ONLY the ParsedReceipt JSON matching the provided
schema. No commentary. JSON only.
```
---
### Call: dedupe-receipts skill
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive an array of ParsedReceipt objects from one trip.
Identify which receipts are duplicates of one another (same
underlying transaction captured via different sources — paper
receipt + Apple Pay screenshot + restaurant confirmation
email).
Match criteria. A pair is a duplicate when AT LEAST TWO of:
- merchant_name_normalised match (case-insensitive)
- total.amount_minor_units match exactly OR within 1% if one
source includes tip and the other doesn't
- transaction_date_iso within 1 day of each other
- last_four match (if both have it)
- merchant_country_iso match
Group all duplicates of one transaction into a DuplicateGroup.
Pick canonical_receipt_id: prefer the source with line items
(typically photo_camera or photo_upload of a paper receipt)
over a screenshot or email confirmation that only has the
total.
Excluded receipts:
- A receipt with is_refund: true AND a refund_of_receipt_id
pointing to another receipt in the trip → exclude from the
settlement and note in excluded_receipts.
- A receipt with ocr_confidence < 0.3 → exclude and note.
- A receipt marked as group_chat_export_claim that has not
been paired with a verified receipt → exclude with reason
"unverified claim".
match_confidence:
- 0.9+ : three or four criteria match, including merchant
and last_four
- 0.7-0.9 : two criteria match, one of which is amount within
tip-tolerance
- 0.5-0.7 : two criteria match but amount differs by more
than tip-tolerance
- < 0.5 : NOT a duplicate, do not group
match_basis lists which criteria matched, exactly.
Hard rules:
- Require AT LEAST TWO criteria in match_basis. Single-criterion
matches (e.g. just amount) are NOT duplicates.
- Do NOT group three receipts as duplicates of each other
unless each pair within the group also satisfies the
two-criteria rule.
- Preserve every receipt_id you received in either unique_
receipts, in a duplicate_groups entry, or in excluded_
receipts. The union of those three sets must equal the
input set.
- When uncertain, do NOT group. The user can manually merge
in the UI; an unnecessary auto-merge is worse than a missed
one.
Output ONLY the DedupedReceiptSet JSON. No commentary.
```
---
### Call: allocate-line-items skill
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive a DedupedReceiptSet, a TripRules object, and any
fetched FX rates. Apply the rules to each receipt's line items
to produce the AllocatedReceiptSet. Each LineItem's
per_person_allocation array gets populated with member_id +
share_amount_minor_units (in the trip's primary_currency).
Application order per receipt:
1. Convert all Money amounts to primary_currency using the
relevant FX rate from the rates input (or 1:1 if same
currency). Round to minor units.
2. Determine which rule applies to this receipt: the per-
receipt override (if present in TripRules.per_receipt_
overrides), else the trip default_rule.
3. Determine which members are eligible for this receipt:
start with all members, then subtract anyone in per_
receipt_tag_outs for this receipt.
4. Apply the rule:
- equal: total / eligible_member_count, allocated equally
to each line item proportionally.
- itemised: per line, eligible members for THIS line =
receipt-eligible members minus any per_line_tag_outs;
line total / line-eligible count. Tip and tax lines
(is_tip_or_tax: true) are allocated proportionally to
each member's pre-tip/tax line totals.
- by_night: line total × (member.nights /
sum(all_members.nights)) per member. Only valid for
lodging categories; falls back to equal otherwise with a
warning.
- by_category: lookup the category-rule mapping in
TripRules; allocate by that rule.
- custom_percentage: line total × (member.percent / 100)
per member.
For paid_by_member_id: if null in the receipt, set it from
the TripRules.payer_assignments (passed in as part of
TripRules); if still null, add the receipt to unresolved_
allocations with reason "no payer assigned".
Rounding: rounding errors accumulate to <$0.05 per receipt by
distributing the remainder to one member (typically the
payer). Sum of per_person_allocation shares MUST equal the
line's total exactly in minor units.
Hard rules:
- Every line item gets a per_person_allocation. If a line
can't be allocated (e.g. no eligible members because everyone
tagged out), add it to unresolved_allocations with a reason
and leave per_person_allocation: null.
- Tip and tax lines (is_tip_or_tax: true) are allocated
proportionally to pre-tip/tax line allocations. They do NOT
use the same flat split as the rest of the receipt unless
the rule is "equal".
- Currency conversions use the rates supplied; do NOT fetch
new rates from your own knowledge.
- Sum of per_person_allocation shares per line == line total
in minor units, exactly.
- Do NOT modify any field of the ParsedReceipt other than the
per_person_allocation on each LineItem and the paid_by_
member_id.
Output ONLY the AllocatedReceiptSet JSON. No commentary.
```
---
### Call: fetch-fx-rate skill
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You fetch the foreign exchange rate for a specific currency
pair as of a specific date. Use google_search grounding to
find a reliable source (oanda.com, xe.com, central-bank
historical data, or ECB / Federal Reserve daily reference
rates).
Output JSON in the text body (NOT via responseSchema —
responseSchema and google_search cannot be combined in the
same Gemini call). The server reads citation URLs from
response.groundingMetadata.
JSON shape:
{
"from_currency": "",
"to_currency": "",
"rate_date_iso": "",
"rate": ,
"rate_source_authority": ""
}
Hard rules:
- Use the daily reference rate for the supplied date, not the
current spot rate.
- If the date is in the future or the source has no data for
that date, return the closest preceding business-day rate
and note it in rate_source_authority ("ECB reference rate
for 2026-03-14, used for transactions on 2026-03-15").
- If no reliable source is found, return rate: null and let
the agent flag the trip for manual rate entry.
- Do NOT include URLs in the JSON body. Citations come from
groundingMetadata server-side.
No commentary outside the JSON.
```
---
### Call: format-export skill
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a Settlement and format it into an ExportArtifact:
deep-link payment intents (Venmo, Cash App, Wise) for each
Transfer, a Splitwise-compatible CSV, and the PDF summary
metadata.
Deep-link formats:
- Venmo: venmo://paycharge?txn=pay&recipients=&
amount=¬e=
(handle is the recipient's Venmo handle if known, else
empty — Venmo will prompt; the user must already know the
handle)
- Cash App: https://cash.app/$/?
note=
- Wise: https://wise.com/pay/?amount=&
currency=&memo=
Memo format (consistent across all three):
" — , settle"
e.g. "Park City May 2026 — restaurant, lodging, lift tickets,
settle"
Memos must be <=80 characters. Truncate the categories list
with "…" if longer.
Splitwise CSV columns:
date,description,category,cost,currency,paid_by,split_type,
member1,member2,member3,...
One row per contributing line item, NOT per Transfer. This
matches how Splitwise expects to import.
PDF summary path:
- pdf_summary_storage_path: a Firebase Storage path of the
form `/settlements/.pdf` (the bucket
is implicit; clients receive a signed URL). This is NOT a
`gs://` URI passed to Gemini — Gemini never reads this PDF.
- The PDF itself is rendered by a separate Cloud Run service;
your output just provides the storage path the renderer
will write to.
Hard rules:
- URL-encode memos; do not include unescaped & or = in the
memo body.
- Deep links must be valid even if the recipient's payment-
app handle is unknown (empty handle is acceptable for
Venmo; the user will fill it in the Venmo app).
- Do NOT include card numbers, last-four, or email addresses
in any memo or CSV row.
- Splitwise CSV uses the currency code in column 5; amounts
are in dollars (not minor units), with two decimals.
Output ONLY the ExportArtifact JSON. No commentary.
```
---
### Call: PDF summary cover illustration
Model: `gemini-3-pro-image` (Nano Banana Pro) · n/a · n/a
```
You generate a single 4K cover illustration for the trip's
finalised settlement PDF. The cover renders the trip name,
the date range, and the names of the trip members as legible
text in-image. Use Nano Banana Pro's typographic capability —
this is the model's flagship strength.
Inputs:
- trip_name (string, e.g. "Park City May 2026")
- date_range (string, e.g. "May 8 – May 11, 2026")
- member_display_names (array of strings, e.g. ["Maya", "Sam",
"Jordan", "Alex"])
Composition:
- Warm muted background — a softly out-of-focus mountain
range, a coastal horizon, a city skyline, or a forest path
depending on the trip's implied context (lodging category
helps: ski lodge → mountains, beach house → coast, city
apartment → skyline). Pick from the implied context; if
ambiguous, default to a soft watercolor texture.
- The trip name is the dominant typographic element, set in a
warm serif or a clean modern sans, centred upper-third.
- The date range is set smaller, beneath the trip name.
- The members' names appear as a horizontal row of small
rounded chips at the lower-third, each chip with the
member's initial in a colored circle + the display name
beside it.
- No actual receipts, no money imagery, no logos.
Hard rules:
- Photographic-realism background with crisp typographic
foreground. The typography MUST be legible at 4K
resolution; this is the Nano Banana Pro skill.
- No people in frame.
- No real brand logos, no real currency images, no real
cards or pay-app screenshots.
- 16:9 aspect ratio for the PDF cover.
- Warm, calm palette; do NOT use the cliché "finance app"
green/blue gradient.
```
---
### Call: hero / empty-state illustration generation
Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a
```
You generate a single photographic-looking image for the
landing page or empty-state of the app. Depicts the universal
moment of receipts piled on a kitchen table, becoming a tidy
settlement, or a phone screen showing the agent's progress.
Prompt anchors that work well:
- "a small pile of paper receipts on a wooden kitchen table,
warm afternoon light from a side window, a phone laid face
up beside them showing a clean settlement table on screen,
no people, shallow depth of field"
- "an open laptop on a coffee table at dusk, the screen
showing a progress log with checkmarks beside each item, a
cup of coffee beside it, no people"
- "a hand holding a phone (partial wrist visible only, no
full person), the screen displays a tidy who-owes-whom
table with three rows, blurred restaurant interior behind"
Hard rules:
- Photographic, not cartoon, not illustration-style.
- No identifiable people in frame.
- No real commercial brand logos (no Splitwise, Venmo, Cash
App, Wise visible).
- Warm light, real-paper texture for receipts; avoid the
glossy AI-render look.
- Aspect ratios: 3:2 for hero, 1:1 for empty states, 16:9
for promotional cards.
```
---
### Call: TTS readback of the settlement (optional)
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: clear, calm, neutral. Pick the Gemini 3.1 Flash TTS
voice whose languageCode matches the user's locale; default
to en-US.
Use case: accessibility readback of the final settlement.
Some users prefer to hear "Maya owes Sam seventy-three
dollars" before they tap through to Venmo.
Pre-process the text before sending to TTS:
- Render amounts as words ("seventy-three dollars" not
"$73"), with the currency name explicit.
- At sentence boundaries, insert an ellipsis ("…") for a
natural pause. Between transfers, insert a blank line plus
an em-dash ("—") to encode a longer pause. Gemini 3.1 Flash
TTS does not support SSML .
- Skip metadata that doesn't belong in spoken readback
(transfer IDs, receipt IDs, FX-rate citations).
- Target rate: ~140 words per minute.
Style direction: prepend ONE short directive sentence,
exactly like: "Read the settlement clearly and calmly, like
confirming a friend's request, with a small pause between
each transfer. …".
There is no separate style API field on Gemini 3.1 Flash TTS;
the directive sentence inside the input is how style is
conveyed.
Phoneme overrides for names, currencies, etc. are NOT
supported. Pronunciation follows the chosen voice's native
locale.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell
you what content the app must support and what the agent must
handle gracefully.
- **Four friends back from Park City, forty-two receipts.** A
long ski weekend. Maya paid the Airbnb. Sam put the lift
tickets on his card. Jordan paid for groceries the first
morning and a restaurant the second night. Alex paid for the
rental car. Receipts are a mix of photos of paper restaurant
bills, screenshots of Apple Pay confirmations, two PDFs from
Airbnb (one for the rental, one for damage-deposit refund),
and three forwarded emails from the lift-ticket vendor. The
agent reads all forty-two, deduplicates eight Apple Pay
shadows of paper receipts, detects the $500 damage-deposit
refund and excludes it from the settlement, and produces
three Venmo intents in ninety seconds.
- **Three couples in a Lisbon Airbnb, multi-currency.**
Receipts are partly in EUR (groceries, restaurants in
Lisbon), partly in USD (the Airbnb charged through a US
card statement). The agent invokes fetch-fx-rate for each
EUR→USD pair (using the receipt's transaction date),
caches the rates, and produces a settlement in the trip's
primary currency (USD) with every FX rate sourced and
cited in the settlement's `fx_rates_used` array.
- **The vegetarian who didn't drink at the steakhouse.** A
group dinner. The bill is $480 + $90 tip + $48 tax. One
diner had a $24 salad and water; the other five had steaks
and wine. The user picks itemised for this receipt (even
though the trip default is equal). The agent allocates
each line to only the diners who shared it; tip and tax
are allocated proportionally to each diner's pre-tip
subtotal. The vegetarian's share is $26.40, not $103.60.
- **Eight cousins at a wedding-weekend rental in
Charleston.** A complex roster: some cousins arrived
Thursday, some Friday, some Saturday. The rental is split
by night: Maya (3 nights) pays 3/16 of the lodging, Sam
(4 nights) pays 4/16, etc. Restaurants are split equally
among everyone present that night. Groceries (which
everyone shared throughout) split equally. The agent
handles three different rules across one trip.
- **Roommates settling a one-off appliance purchase.** Two
roommates bought a couch together for $1,200. One paid;
the other Venmo'd half later but it bounced. The user
drops the IKEA receipt and the bounced Venmo notification
into the app. The agent treats the bounced Venmo as a
refund (so the original payment stands) and produces one
transfer: roommate 2 owes roommate 1 $600.
- **The group-chat claim that wasn't a receipt.** Sam pasted
the iMessage export from the trip. The agent extracts
"Maya: I paid for the boat rental, $180" as an unverified
claim. The app surfaces it on the review pane with a
prompt: "no receipt attached — confirm this claim or
upload the receipt". Maya later forwards the boat-rental
confirmation email; the agent matches the email to the
claim and promotes it to a verified line. The settlement
re-runs.
- **The low-confidence receipt that needs human review.**
One of the photos is a faded thermal-paper restaurant
receipt taken in low light. OCR confidence parses at 0.62.
The agent flags it in the review pane with the parsed
fields and the photo side-by-side. Sam corrects the total
from $128.40 to $138.40 and confirms. The settlement
re-runs.
- **The damage-deposit refund that came two weeks later.**
The Airbnb damage deposit ($500) was charged at booking
and refunded after checkout. The booking receipt is a
PDF; the refund is an Apple Pay screenshot showing a
green "+$500.00" with a return arrow. The agent
recognises the refund and excludes it from the
settlement — Maya doesn't get charged the $500.
- **The trip whose primary payer is unclear.** One receipt
in the pile is an Apple Pay screenshot with last-four
4827. Two members of the trip have cards ending in 4827.
The agent flags `paid_by_member_id: null` and surfaces a
two-tap chooser in the review pane: "who paid this one?
Sam or Alex". The user taps; the settlement re-runs.
- **Past trips, six months later.** Someone messages the
group: "wait, did we ever settle that Vegas weekend?"
The trip is still in the user's archive. They tap the
trip; the settlement is there, every receipt preserved.
They send the Venmo intents to the group again; nothing
about the past trip has been altered.
- **The dispute thread.** Maya disagrees with the agent's
allocation of the $42 surfboard rental — she insists she
didn't go on the boat that day. She taps the line, opens
a dispute, types "I was at the cabin with Alex that
morning, ask him". The agent re-runs with Maya tagged
out of that line; the settlement updates; Alex gets a
notification asking him to confirm Maya's tag-out.
## 6. Page structure
The app is a single-page web app, mobile-first, that also runs
as a Workspace-integrated dashboard and (post-I/O 2026) a
native Android build via AI Studio Build's Kotlin export.
### Landing / unauthenticated
- **Hero block:** the tagline ("Drop the receipts in. A
sandboxed agent reads, splits, and settles them — every line
traceable, every transfer your call."), a single primary
button (**Try a sample trip** + **Sign in with Google**),
and a Nano Banana 2 hero illustration: receipts on a wooden
table becoming a tidy settlement on a phone screen.
- **The 30-second demo strip:** an inline animated demo of
the streaming agent log. Four cards: "drop receipts", "agent
reads", "agent splits", "tap to send". Each card is a
screenshot, not a video, so it loads fast.
- **What it does vs Splitwise / spreadsheets** — a short
three-row comparison: Splitwise (manual entry), spreadsheets
(math at the airport), Receipts → Who Owes Whom (drop and
tap).
- **Privacy promise** — receipts stay private to the trip's
members; the Managed Agent's sandbox is ephemeral; no
training-data usage.
### Authenticated home — Trips
- **Active trips list** — trips with unsettled receipts at the
top; archived trips below
- **New trip button** — opens a small form: trip name, date
range, member invites (emails or phones for magic links),
default split rule, primary currency
- **Per-trip card** — trip name, dates, member avatars, count
of receipts uploaded, count flagged for review, current
settlement status (no settlement yet / settled / re-run
pending)
### Trip detail page
- **Top row** — trip name, date range, member chips, the
unique forwarding email address (e.g. `parkcity-2026-may@
in.receipts.app`) with a copy button
- **Drop zone** — large camera / file-upload area; supports
drag-and-drop, multi-file selection, HEIC / JPEG / PNG /
PDF
- **Receipts list** — every uploaded receipt as a card with
thumbnail, merchant, amount, who paid, status (parsed /
flagged / excluded). Tappable to open the review pane.
- **Settle button** — large, primary, only enabled when the
flagged count is zero. Tapping kicks off the agent run.
- **Streaming agent log panel** — appears below the Settle
button once the run starts. Shows progress events in real
time with the agent's status line, a count of receipts
parsed, a count of duplicates found, and a count of
remaining steps.
- **Settlement table** — appears after the run completes. A
matrix of who-owes-whom with the transfer count ("3
transfers — would have been 6"), each cell tappable to
drill into contributing receipts.
- **Export buttons** — Venmo all, Cash App all, Wise all,
download Splitwise CSV, download PDF summary.
### Per-receipt review pane
- **Photo / PDF preview** — left side
- **Parsed fields** — right side, each field editable;
merchant, date, total, line items (with category dropdowns),
paid_by member chooser
- **Allocation preview** — at the bottom, shows how the
current split rule would allocate this receipt across
members
- **Re-run parse** — a small button that re-invokes the
parse-receipt skill on this receipt only (useful when the
user pastes a corrected version)
- **Mark as duplicate** — a small button that links this
receipt to another existing receipt in the trip as a
duplicate
### Trip settings
- Default split rule (changeable post-hoc; settlement will
re-run if changed)
- Primary currency
- Per-receipt rule overrides — a list
- Per-receipt and per-line tag-outs — a list
- Nights-per-member (for by-night rule)
- Custom-percentage rules (for the custom rule)
- Member roster — invite, remove (member removal preserves
their past contributions but they lose access)
### Member personal view
When a member who is not the trip owner opens the trip, they
see a subset:
- Their personal owe / owed summary at the top
- The list of receipts THEY uploaded (others' uploads are
visible too but tagged with the uploader's avatar)
- Their per-receipt tag-out controls
- The dispute thread, if any
- The Venmo / Cash App / Wise buttons for transfers
involving them
### About / Capabilities info
A small page (linked from the footer) describing the
capabilities, the model IDs used, and the preview-status
disclaimer for the Managed Agents API.
### Settings (per-user)
- Connected payment app handles (Venmo handle, Cashtag, Wise
recipient ID) — used to pre-fill payment intents
- Default trip preferences
- Data export and account deletion
## 6b. First-visit onboarding
The first time a user lands on the authenticated home, surface a
short three-step onboarding overlay — non-blocking, dismissable —
that walks through:
1. **Create your first trip** — name, dates, primary currency,
invite a friend (or skip and run a sample trip pre-populated
with fake receipts to feel the agent run before any real data)
2. **Drop a few receipts** — show the drop zone with a brief
note: "photos, PDFs, screenshots, email forwards, all
welcome. The agent reads all of them"
3. **Tap Settle** — explains that the Managed Agent will run
for 60–180 seconds, show a streaming log, and produce a
settlement table; nothing about money moves until the user
taps a payment-intent button
The onboarding remembers progress per user — partially-done
onboarding picks up where the user left it. Dismissible at any
step; re-openable from Settings.
Inside the trip page, on first arrival, surface a small inline
tip: "tip: forward your Airbnb / restaurant confirmation emails
to `@in.receipts.app` to add them automatically".
## 6c. Capabilities info button
A small **i** icon in the footer of every page opens a panel
titled "How this app works" with these sections:
- **Hero capability: Managed Agents API (preview)** — explains
that when the user taps Settle, a sandboxed Linux agent
provisions and runs the parse → dedupe → allocate →
compute → format pipeline. The agent's progress is streamed
in real time. Notes the preview status and that the agent's
behaviour is defined by the AGENTS.md + per-skill SKILL.md
files in this template's source.
- **Multimodal receipt parsing** — Gemini 3.5 Flash reads
every receipt image / PDF / screenshot and produces a
structured `ParsedReceipt`.
- **Currency rates via grounded search** — when a trip is
multi-currency, FX rates are fetched per (from, to, date)
from authoritative sources (ECB, Federal Reserve, oanda)
via google_search grounding. Every rate cites its source.
- **Settlement is deterministic code** — explains that the
minimum-transfer settlement is a graph reduction computed
in Python inside the agent's sandbox, NOT an LLM
inference. The model handles the unstructured work (read
the receipt, decide what's a duplicate, apply the rule);
the math is code.
- **Privacy** — receipts are stored in the trip's private
Firebase Storage bucket, only visible to invited members.
The Managed Agent's sandbox is ephemeral — destroyed when
the run completes. No receipt content is used to train
Google's models. We use the Gemini API on the paid tier,
where Google does not use your content for model training,
per the Gemini API Additional Terms.
- **What we don't do** — we don't move money. Every transfer
requires the user to tap through to their chosen payment
app and authorise. We don't store payment credentials.
## 6d. Detailed cost breakdown
Per settlement run on a typical 30-receipt, 4-member trip:
- **Managed Agent run** (`antigravity-preview-05-2026`):
preview pricing as of 2026-06-01 is bundled per-session;
estimate $0.20–$0.40 per run including the sub-calls the
agent makes. **Preview pricing may change at GA.**
- **parse-receipt sub-calls** (`gemini-3.5-flash`, 30
invocations): 30 × ~2k input tokens (image + system
instruction) + ~500 output tokens = ~75k tokens. At
$1.50 / $9.00 per 1M tokens for global pricing:
~$0.11 for input + ~$0.14 for output ≈ **$0.25**.
- **dedupe-receipts sub-call**: ~30k input + ~3k output =
~$0.07.
- **allocate-line-items sub-call**: ~30k input + ~5k output
= ~$0.09.
- **fetch-fx-rate sub-calls** (only if multi-currency):
typically 1–3 calls per trip, ~$0.005 each.
- **compute-settlement** (Python in sandbox): no Gemini
cost; sandbox compute included in agent session.
- **format-export sub-call**: ~10k input + ~3k output ≈
$0.04.
- **PDF summary cover** (`gemini-3-pro-image`, once per
finalised settlement): approximate ~$0.06 per image at
the published $2/$12 per 1M token Pro Image rates (Google
has not pinned an exact public per-image figure; verify
before shipping).
- **Cloud Run compute** (the `runSettlement` function +
SSE streaming): negligible at free-tier; ~$0.001 per run.
- **Firestore reads/writes**: ~200 ops per settlement ≈
$0.0002.
- **Firebase Storage**: receipt storage at $0.026/GB/month;
a 30-photo trip is ~30 MB ≈ $0.001/month.
**Total per settlement: roughly $0.40–$0.80.** A heavy user
with 12 trips a year would spend under $10/year on backend
costs. Free for personal use is feasible at typical volumes;
a "club" tier at $4/month for unlimited trips + advanced
exports would have ~95% gross margin.
## 7. Design language
The aesthetic is **product**, not memoir. This is a tool. Calm,
confident, fast.
- **Type:** SF Pro (Apple system font stack) — `-apple-system,
BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica
Neue", Arial, sans-serif`. Body 16px on mobile, 17px on desktop,
line-height 1.55. Headings 28/22/18px. Monospace for amounts,
receipt IDs, and the agent log: `"SF Mono", "JetBrains Mono",
Menlo, monospace`.
- **Palette:** warm neutrals, not finance-app green.
Background `#fbfaf7` (warm off-white). Card background
`#ffffff`. Border `#ece9e2`. Primary text `#1a1916`. Secondary
text `#6c6a64`. Accent (primary action, tappable cells): a
muted persimmon `#c4541e` with soft tint `#fbe9de`. Secondary
accent (the agent's status / progress log): a soft slate
`#3a4654` with tint `#e8ebee`. Success (settled): `#3d7a4d`
with tint `#dfeede`. Warning (flagged for review): `#a8762a`
with tint `#f9efd9`. Error (excluded / rejected): `#a13d3d`
with tint `#f5dfdf`. No bright finance-blue. No fintech
gradients.
- **Receipt thumbnails:** displayed at a consistent 3:4 aspect
ratio with a thin warm border; OCR-flagged thumbnails get a
warning-tint corner badge.
- **Amount typography:** monospace, slightly tabular-num, so
columns of amounts align vertically. Negative amounts in the
warning color, never red unless they're refunds (different
styling).
- **Agent log:** monospace, slate background, white text;
each event has a small status icon (parsing / dedupe /
allocate / settle / done) and an elapsed-time stamp. The log
visually feels like a developer terminal, intentionally — it
is the proof-of-work surface.
- **Settlement table:** rows for each "owes" relationship,
amounts right-aligned in monospace, every row tappable to
drill into the contributing receipts. The visual hierarchy
makes the table itself the page's hero element.
- **Buttons:** clear rectangular buttons with 8px radius;
primary action (Settle, Send Venmo) is filled accent; secondary
is outlined; tertiary is text-only. No flashy gradients, no
glow effects.
- **Mobile vs desktop:** mobile is the primary surface (the
user is on a tray table or in an Uber from the airport).
Desktop is for the deeper review work (the user is at a
laptop sorting out the messy multi-couple trip).
- **Motion:** restrained. The agent log animates one event at
a time with a 100ms fade-in per row; nothing else animates.
The settlement table appears with a single 200ms opacity
transition once the agent completes. No spinning loaders;
the streaming log IS the loader.
- **Empty states:** illustrations from Nano Banana 2, warm,
photographic, no people. Always include a single primary
action ("create your first trip", "drop your first receipt").
## 8. Content generation rules
The content generated by Gemini in this app is in three buckets:
1. **Parsed receipt data** — verbatim from the receipt where
possible, with a normalised secondary field. Never invent.
2. **Agent rationale strings** — short, 1–2 sentences per
Transfer, citing the contributing receipts by their human
labels (merchant + date) not by their IDs.
3. **Illustrations** — see section 9.
Rules across all generated content:
- **Verbatim preserved.** Every parsed receipt preserves the
original-language merchant name, line-item descriptions, and
category strings in `_verbatim` fields. A normalised version
lives in a separate field. No silent translation.
- **No invented data.** The agent's progress log, the
settlement, and every Transfer cite real receipt_ids. The
server validates that every contributing_receipt_id exists.
- **Audit-friendly language.** The agent's rationale for each
transfer reads like an accountant's line, not a marketing
blurb. "$73 = Maya's share of the lift tickets (lift-tickets
receipt May 8, $292 / 4 members) + Maya's share of the
Saturday dinner (Casa Verde May 9, $186.40, equal split)."
- **No moralising about spending.** The app does not comment
on what the trip cost, how much beer the group drank, or
whether the Airbnb was overpriced.
- **Currencies always explicit.** Every amount displayed
includes the currency. Never a bare "$73" in a trip whose
primary currency is EUR.
- **Confidence surfaced.** Low-confidence parses display the
confidence number and the specific field that's uncertain.
No silent "this might be wrong" hedging.
- **Errors readable.** When the agent fails (preview-tier
quota exhausted, sandbox timeout, etc.) the error message
is a short, specific sentence and the fallback path is
named ("falling back to sequential parsing — this will take
a few minutes longer").
## 8a. Seed content
Pre-seed the app with one **sample trip** the user can run end-
to-end without uploading anything, so the demo "tap Settle and
watch the agent work" lands within the first 60 seconds of the
first visit.
**Sample trip:** "Park City — May 2026", four fictional members
(Maya, Sam, Jordan, Alex), 18 pre-loaded receipts covering:
- 1 Airbnb confirmation PDF ($1,240 for 3 nights)
- 1 Airbnb damage-deposit refund Apple Pay screenshot (+$500)
- 4 lift-ticket email confirmations ($292/person/day for 4
days, 4 people)
- 5 restaurant photos (paper receipts in varied lighting; one
is intentionally a bit blurry to demonstrate the flag-for-
review flow)
- 3 grocery receipts (the first morning, a mid-trip resupply,
a final-day snack run)
- 2 rental-car receipts (the rental + one fill-up)
- 1 Apple Pay screenshot duplicating one of the restaurant
paper receipts (to demonstrate dedup)
- 1 group-chat-export paste claiming "Maya: I paid for the
spa, $180" with no attached receipt (to demonstrate the
unverified-claim flow)
The sample trip is pre-configured with default rule "itemised
for restaurants, equal for everything else" and primary
currency USD. Tapping Settle runs the real agent on the real
receipts and produces a real settlement table. The user can
explore the review pane, the dispute flow, and the export
buttons — all without uploading their own data.
A small "this is a demo trip" banner sits at the top of the
sample trip's page with a "delete demo and start fresh"
button.
## 9. Media & assets
- **Hero illustration** (landing page): Nano Banana 2
(`gemini-3.1-flash-image`), 3:2 WebP at 2048×1365, generated
once at build time. Prompt: "photographic warm kitchen-table
scene at late afternoon light, a small loose pile of paper
receipts on the left curling at the edges, a phone on the
right showing a clean settlement summary screen with three
rows of names and amounts, soft shadow under the phone, real
worn paper, no people, no commercial branding, slight
imperfection". Ship as a seed asset at
`/public/illustrations/hero.webp`.
- **Empty-state illustrations** (3 files: no trips yet, no
receipts in a trip yet, settlement not yet computed): Nano
Banana 2 (`gemini-3.1-flash-image`), 1:1 WebP at 1024×1024
each, generated once at build time. Prompts: (a) no trips —
"photographic empty wooden table at golden hour with a single
unopened paper envelope in the centre, no people, no text";
(b) no receipts — "photographic empty wooden tray on a
kitchen counter with a phone face-down beside it, soft warm
light, no people, no text"; (c) settlement not computed —
"photographic open paper notebook on a table with a single
pencil resting across a blank page, warm overhead light, no
people, no text". Ship as seed assets at
`/public/illustrations/empty-trips.webp`,
`/public/illustrations/empty-receipts.webp`,
`/public/illustrations/empty-settlement.webp`.
- **Demo strip cards** (4 cards in the 30-second demo strip):
Nano Banana 2 (`gemini-3.1-flash-image`), 4:3 WebP at
1024×768 each, generated once at build time. Per-card
prompts: (1) drop — "photographic hand holding a paper
receipt above a phone screen showing the upload zone, warm
light, no faces, no commercial branding"; (2) read — "phone
on a wooden table showing a parsed receipt with line items
highlighted as chips, warm light, no people"; (3) split —
"phone showing a split view with three avatars and amounts
beside each, warm light, no people, no real names visible";
(4) send — "phone showing a send-confirmation chip with a
green check, warm light, no people". Ship as seed assets at
`/public/demo/strip-{1..4}.webp`.
- **Per-settlement PDF cover**: Nano Banana Pro
(`gemini-3-pro-image`), 16:9, generated at runtime per
settlement, stored in Firebase Storage at
`trips/{tripId}/settlement-cover.png`. Prompt template (filled
per trip): "photographic muted-paper background with the trip
name '' rendered crisply at 4K in a warm display
serif at the top-left, the date range ' – '
rendered legibly in a smaller sans-serif beneath, no people,
no commercial branding, real paper texture, soft golden
light".
- **Member avatars**: generated initials-on-color chips by
default; user can upload a real photo if they want.
- **No real brand logos** anywhere — no Venmo, Cash App,
Splitwise, Wise, Apple Pay logos appear in illustrations.
In the export buttons, brand names appear as text labels
only.
- **Receipt thumbnails**: the user's actual uploaded photo
/ PDF; never replaced with a stock image.
All AI-generated images are credited in the about page
("illustrations generated with Nano Banana 2 and Nano Banana
Pro").
## 10. Interactivity & states
- **Loading states:** every async action shows a clear state.
The big one is the agent run, which streams events; smaller
ones (saving a tag-out, marking a duplicate) show a
one-line status next to the affected element.
- **Empty states:** never blank. Always an illustration + a
short helpful sentence + a primary action.
- **Error states:** when the Managed Agents API returns an
error (preview quota, timeout, sandbox failure), surface a
clear card naming the error AND offer the fallback path
("run sequential parsing without the sandbox"). The
fallback is slower but produces the same shape of output.
- **Disabled states:** the Settle button is disabled with a
tooltip when the flagged-review count is > 0. The tooltip
names the count and links to the review pane.
- **Confirmation states:** every destructive action (delete
a receipt, remove a member, delete a trip) requires a
confirmation dialog.
- **Optimistic updates:** uploading a receipt and editing a
parsed field both update the UI optimistically with a
visible "saving…" indicator; rollback on server error.
- **Real-time:** the streaming agent log uses Server-Sent
Events from the Cloud Run function. The client reconnects
automatically if the connection drops. The full event log
is replayable from the persisted `agent_runs` collection
in Firestore.
- **Per-receipt review pane:** edits to the parsed fields
save on blur with a debounce. Editing a critical field
(total, paid_by, currency) prompts to re-run the
settlement.
- **Drag-and-drop:** on desktop, the receipts list supports
drag-and-drop reordering (useful when a user wants to
visually group by category before reviewing). The order
is cosmetic; the settlement is unaffected.
- **Keyboard shortcuts:** `n` for new trip, `u` for upload,
`s` for settle (when enabled), `r` for re-run, `?` for
the help / shortcut list.
- **Touch:** every tap target is at least 44x44px. Swipe-left
on a receipt card opens the review pane; swipe-right marks
as duplicate.
## 11. Tech & responsive requirements
- **Stack:** AI Studio Build default — TypeScript + React +
Vite, served from Cloud Run; Firestore + Firebase Auth +
Firebase Storage; the Managed Agents API for the settlement
run; Cloud Run server functions for orchestration; Secrets
Manager for the Gemini API key.
- **Mobile-first.** 320px (iPhone SE) is the minimum width.
Every component lays out cleanly at 375px and 414px.
- **Tablet.** 768px (iPad portrait) gets a two-column layout
on the trip detail page (receipts list left, agent log /
settlement table right).
- **Desktop.** 1024px+ gets a three-column layout (trips list
left rail, trip detail centre, review pane right rail).
- **Fluid typography.** Use `clamp()` for every heading and
body size (e.g. `clamp(0.95rem, 0.85rem + 0.4vw,
1.05rem)` body, `clamp(1.6rem, 1.2rem + 2.4vw, 2.6rem)`
for the settlement-total figure) so type scales smoothly
from 320 px up to 2560 px without snap-y breakpoints.
- **Container queries.** Per-component responsiveness
(receipt card, agent log, settlement table) uses
`container-type: inline-size` so each component reflows
based on its own width — not the viewport's — which is
what the three-column desktop / two-column tablet
layouts demand.
- **Viewport units.** Use `dvh` / `svh` / `lvh` instead of
`vh` everywhere a layout pins to viewport height (the
full-screen camera capture, the receipt-review overlay,
the settlement-running state). Never `100vh` — iOS
Safari's URL bar will clip it.
- **Safe-area.** The sticky bottom action bar on mobile and
the floating capture FAB honour
`env(safe-area-inset-bottom)`; the top trip-title strip
honours `env(safe-area-inset-top)`. The viewport meta
tag includes `viewport-fit=cover` so backgrounds extend
edge-to-edge under the iOS notch / Dynamic Island.
- **Native Android (post-I/O 2026 AI Studio Build target).**
The Kotlin + Jetpack Compose export retains the same screen
structure with native-feeling navigation; the streaming
agent log uses a `LinearProgressIndicator` + a rolling list
of status lines. Camera capture uses CameraX.
- **Workspace integration.** A Sheets-backed view of any
trip's settlement (read-only) lets a non-app member of the
group see the result without installing anything. Uses the
post-I/O 2026 Workspace integration that AI Studio Build
provides without OAuth gymnastics.
- **Mobile AI Studio Build app** (rolling out post-I/O 2026)
— if the user has it, they can take photos of receipts
inside Build and have them push into the trip directly.
Flag as "rolling out — available where AI Studio Build
mobile is enabled".
- **Performance:** initial page load under 2s on a 4G
connection (the hero image is preloaded; everything else is
lazy). The agent log starts streaming within 1s of tapping
Settle. The settlement table renders within 200ms of the
final "done" event.
- **Offline.** Receipts uploaded offline are queued in
IndexedDB and pushed when connection returns. Settlement
itself requires the agent and therefore requires
connectivity.
- **Browser support:** modern Safari (iOS 18+, macOS 14+) is
the priority; Chrome on Android + Chrome on macOS / Windows;
Firefox best-effort. No IE.
- **No `transition: all`.** Every transition lists the exact
properties it animates.
- **Images:** WebP for thumbnails; original HEIC / JPEG / PNG
preserved in Firebase Storage; AVIF generated on the fly for
display.
- **Bundle size:** under 200KB gzipped for the initial route;
trip-detail view lazy-loaded.
- **Local save fallback (FSA non-Chromium):** the "download
Splitwise CSV / settlement PDF" affordance uses
`showSaveFilePicker()` on Chromium; on Safari / Firefox, fall
back to an `` element pointed at a blob URL — FSA
`showSaveFilePicker()` is not supported there.
## 12. Accessibility (WCAG 2.2 AA)
- **Keyboard navigation:** every action has a keyboard path.
Tab order follows visual order; focus rings are always
visible (never `outline: none` without a replacement).
- **Screen readers:** every receipt card has an accessible
name (merchant + date + amount). The agent log is an
`aria-live="polite"` region so streaming events are
announced. The settlement table uses real `` markup
with `| ` row and column headers.
- **Colour contrast:** body text ≥ 4.5:1 against background;
amounts ≥ 4.5:1; warning / error states ≥ 3:1. The
persimmon accent on white tested at 5.1:1.
- **Targets:** all tap targets ≥ 44x44px on mobile.
- **Focus visible:** every focusable element gets a 2px
accent ring with 2px offset on focus.
- **Reduced motion:** `@media (prefers-reduced-motion:
reduce)` disables the agent-log fade-in, the settlement-
table opacity transition, and any other motion. The log
still updates; it just doesn't animate.
- **TTS accessibility option:** in the settlement view, a
"read aloud" button invokes the Gemini 3.1 Flash TTS
preview to read the settlement (see section 4b). Subtitles
appear on screen as the audio plays.
- **High-contrast mode:** respect `prefers-contrast: more`;
borders darken, accents intensify.
- **Text resize:** layout holds up to 200% browser zoom
without horizontal scroll.
- **Form errors:** inline, near the offending field, with a
text explanation (not just a colour change).
- **Magic-link emails:** plain text + HTML versions; the HTML
version uses semantic markup; both include a clear plain-
text login URL.
## 13. Quality bar — avoid AI clichés
- **No "powered by AI" branding.** The capabilities-info
panel names the models honestly; the marketing surface
talks about what it does, not how. "Drop receipts, get a
settlement" beats "AI-powered expense splitting" every
time.
- **No emoji-heavy copy.** The product feels confident, not
excitable. The settlement table reads like an accountant's
ledger.
- **No fake testimonials.** If a quote appears in the
landing page, it is clearly labelled as illustrative.
- **No invented urgency.** No "settle in 60 seconds!" timer
pressure. The agent runs as fast as it runs; the user is
not racing anyone.
- **No fintech blue / green gradient.** The palette is warm
neutrals; persimmon accent.
- **No glow effects, no animated counters, no confetti when
a settlement completes.** A finished settlement is a quiet
success state, not a celebration.
- **No "magical" framing of the agent.** It is a sandboxed
Linux agent running tools. Calling it a "magical assistant"
cheapens it. The honest framing — "this is a Linux sandbox
that reads your receipts and computes a settlement" — is
more impressive.
- **No invented receipts in the demo trip.** Every receipt
in the sample trip is content this author composed; no real
brand logos, no real restaurant names that overlap with
real businesses (use fictional names like "Casa Verde",
"Pine Diner", "Anchor Coffee" that don't map to real
restaurants).
- **No AI-render aesthetic in the illustrations.** Nano
Banana 2 and Pro are prompted for warm photographic light,
real-paper texture, no people, no people, no glossy
artificial surfaces.
- **No over-friendly chatbot voice.** Status messages are
factual ("parsed 12 of 47", not "I'm parsing your receipts
now, this is so exciting!"). The product respects the
user's time.
## 14. Deliverables
- A working AI Studio Build app at the URL the user gets
when they remix this template.
- Auto-provisioned Firebase Auth (Google sign-in), Firestore,
Cloud Run; **the builder enables Firebase Storage manually**
per the auth requirements in section 4b.
- The Managed Agents API session pre-configured with the
AGENTS.md + per-skill SKILL.md files from this template's
source.
- A pre-seeded sample trip ("Park City — May 2026") with
18 receipts that can be settled end-to-end on the first
visit.
- Email-forwarding ingestion wired up via SendGrid Inbound
Parse (or AWS SES + Lambda) — the user provides the
forwarding domain.
- The fallback "sequential parsing without sandbox" path
wired up for graceful degradation when the Managed Agents
API preview returns an error.
- A `.env.example` listing every required secret: Gemini
API key, Firebase service account JSON, SendGrid webhook
secret, Cloud Run service URL.
- A short `README.md` for the remixer: how to provision
Firebase Storage, how to configure the email-forwarding
domain, how to add Apple sign-in (optional), how to add
payment-handle pre-fills per user.
- A "capabilities info" panel surfaced from the footer that
honestly names the models, the preview status, and the
privacy posture.
- Native Android export via AI Studio Build's Kotlin target
enabled by default (the export button is visible; the
builder can publish to Play Internal Test from inside
Build).
- WCAG 2.2 AA conformance per section 12.
- Optimised image pipeline (WebP thumbnails, AVIF served on
the fly).
- Bundle under 200KB gzipped for the initial route.
- Initial load under 2s on 4G.
This is one template. Build it well, ship it, watch a visitor
drop their post-trip receipts in and tap Settle.
|