# 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.