Expense chat
Expense chat fills the existing New expense form from a short conversation. It uses Mistral Large 3 on Bedrock (mistral.mistral-large-3-675b-instruct, us-east-1). Web and desktop share ExpenseChatBox; native mobile uses the same API hooks and existing rewarded-ad credit earning.
The model proposes one expense for editable review. Review expense replaces the form only after explicit user action; all fields remain editable. Only Add expense writes an expense through the existing service. Chat preserves calendar dates, currencies, all actual payers/card charges and supported splits. Receipt scanning keeps its independent Ministral 8B model.
Limits and billing
- Five accepted user messages per conversation; fifty per user per UTC day across groups/devices.
- 2,000 characters per user message, plus a separate 4,000 input-token bound over the complete serialized inference request.
- At most 1,000 output tokens and one paid model invocation per accepted turn. No automatic SDK retry, model repair or expensive fallback.
- Sessions last 24 hours. Reads enforce expiry before eventual DynamoDB TTL cleanup. Relative dates use the fixed session-local today and a validated IANA timezone.
- The displayed integer credit price is echoed by the send request. Launch requires 25 credits/message under the approved pessimistic revenue assumption and cost ceiling below.
- Accepted failed/truncated/unusable attempts remain charged, like receipt scans. Invalid/unauthorized requests, insufficient funds, ambiguity/context rejection, status reads, reviews, saving and identical transport retries cost no additional credits.
- Ten sends and six session creates per user per minute, plus gateway/shared throttling. Global daily admission reservations stop at USD 0.10; at the USD 0.004 floor this allows at most 25 turns globally, before the per-user daily cap.
Before sending, clients show price, balance, remaining messages and failed-attempt policy. Manual completion remains available. Mobile reuses the existing earn-credit action; five credits are granted per rewarded ad.
Model contract and input accounting
The worker makes one native InvokeModel request with a named, strict JSON schema: either question plus expense=null, or a complete expense plus question=null. A nonempty question always discards any accompanying partial financial proposal. JSON returns data, never a tool execution or expense write. Interrupted output, multiple completions, unsupported payload versions/models and invalid financial data are rejected without another inference. A provider refusal becomes a fixed expense-only prompt without provider text or a draft.
Only selected-group currency and indexed participant names are supplied. The authenticated caller is identified explicitly; first-person input in every language resolves to that caller. UUIDs are sorted and bound to the private attempt payload; model indexes map back only to authorized roster UUIDs. Participant/currency fingerprints prevent later turns using a changed roster.
Accepted user descriptions and actual clarification questions are retained as server-owned conversation turns in one JSON input. Fixed review acknowledgements are omitted; financial facts are never silently truncated. Corrections override earlier values while retaining unchanged payer/date/currency/split facts. Questions need no model-generated memory. The current draft/memory fallback is used only without conversation history.
The conservative local input bound is the UTF-8 byte length of the entire serialized native inference body, including schema, instructions, roster and history, plus 256 tokens for the bounded chat template. Byte-fallback tokenization cannot produce more ordinary tokens than input bytes; the wire envelope/schema and template reserve overestimate normal requests. This is not a characters/4 estimate. Unicode, large rosters and long histories are checked before admission, without tokenizer downloads or an extra provider call. The response offers manual completion when facts cannot fit. Provider-reported token usage is also checked after completion and recorded without content.
Stored payload protocol mistral-json-v7 seals the priced model ID, group currency, authenticated caller index and roster indexes. Older representations fail before paid execution so formats cannot be confused during deployment. A caller absent from the current roster is rejected before admission or charging.
Money and missing facts
Money is copied as major-unit decimal strings. The server converts with the shared ISO currency exponent and BigInt arithmetic: amounts, item costs and amount divisions use expense currency; fxCharge uses sealed group currency. JPY "1200" becomes 1200 minor units; BHD "1.234" becomes 1234; a GBP "6" card charge becomes 600. Same-currency charges derive from the paid amount. Percent/share weights are checked integers. Gross purchases and negative discount lines remain separate.
The parser accepts whitespace and the expected currency's ISO code or canonical narrow symbol, such as GBP6, 6 GBP or £6. Currency comes from validated expense/sealed group context; labels never select or change it. Mismatched units, ambiguous separators, nonzero excess precision, unsafe magnitudes and numeric-type coercion are rejected. Trailing zero precision does not change a value. No FX rate, rounding or monetary repair is inferred.
An empty EQUAL split resolves to the sole authorized member of a one-person group: there is exactly one possible group audience. Multi-person groups still require explicit participants or a clarification, and missing weighted divisions never receive a default. Gross purchase prices and separate negative discount lines are copied without netting the discount into the purchase price again; payments copy the stated net paid amount.
Before returning a draft, the adapter sums items and payer amounts using BigInt, requires safe aggregate totals and exact equality, and never changes a payer amount to force reconciliation. Existing domain validation still checks participants, splits, payments and charges before storage/saving. Blank descriptive labels receive neutral editable defaults; unused equal-split weights are ignored.
A structurally valid proposal with no payer asks who paid; no item/split participants asks how to split. Missing actual FX charges ask for each payer's group-currency charge. These results contain no draft or partial financial data. Fully specified live cases still require correct complete drafts. Unqualified group-currency amounts beside individual payments, including parentheses, are stated charges; explicitly estimated or absent values require clarification.
Duplicate names
Chat requires distinct participant names after Unicode NFKC, whitespace and case normalization. Duplicate/blank names reject preparation with CHAT_AMBIGUOUS_ROSTER before credits, counters, dollar reservation or inference. Assign distinct names and start a new chat, or finish manually. This conservative rule also applies to first-person descriptions in such groups; the model cannot override it.
API and persistence
All five routes require the app JWT and exclude developer tokens. Current membership and WRITE_OWN_EXPENSE are checked on every operation, including reads. Sessions are private to their creator and selected group.
| Operation | Request | Result |
|---|---|---|
GET /users/me/expense-chat-policy |
— | Enabled state, displayed credit price and caps |
POST /groups/{groupId}/expense-chats |
{chatId,timeZone} |
Uncharged, recoverable session |
GET /groups/{groupId}/expense-chats/{chatId} |
— | Transcript/status, draft, version and allowance |
POST /groups/{groupId}/expense-chats/{chatId}/messages |
{messageId,text,expectedVersion,expectedCreditsPerMessage} |
Charged admission, async result recovered by GET |
POST /groups/{groupId}/expense-chats/{chatId}/expense |
Reviewed CreateExpenseRequest |
Explicit confirmed expense |
Admission atomically debits credits, appends SPEND_EXPENSE_CHAT, increments conversation/daily counts, reserves global daily dollars, and stores the attempt plus durable outbox task in UserData. An immediate async Lambda invocation starts work. EventBridge redelivers unclaimed outbox tasks every minute; only one worker can claim an attempt. SDK attempts are one, provider abort is 20 seconds, worker timeout is 30 seconds and concurrency is two. RUNNING work is not reclaimed for another inference. A 120-second attempt deadline makes abandoned work a charged failure.
Matching message ID/text replays return the stored session, even after disablement; different text returns conflict. Session versions change on admission, claim and completion. Active work/stale versions return 409. A price mismatch returns CHAT_PRICE_CHANGED before debit. Expiry returns 410; insufficient funds/context/ambiguous roster return 422; daily/global limits return 429 with retry time to the next UTC day; disabled/unverified economics return 403.
Stable reason prefixes include CHAT_MESSAGE_ID_CONFLICT, CHAT_CHANGED, CHAT_MESSAGE_LIMIT, CHAT_CONTEXT_LIMIT, CHAT_AMBIGUOUS_ROSTER, CHAT_GROUP_CHANGED, CHAT_PRICE_CHANGED, INSUFFICIENT_CREDITS, CHAT_DAILY_LIMIT, CHAT_BUDGET_LIMIT, CHAT_EXPIRED and EXPENSE_CHAT_DISABLED.
Confirmation uses the stable session expense ID and a reviewed-request hash. Expense, balances, notification and saved marker commit atomically. Duplicate confirmations return the same expense; they cannot recreate a deleted expense or silently accept changed content. Manual completion may still be saved after a later clarification clears the draft.
Economics and settings
SSM /tailspend/{stage}/expense-chat/settings starts disabled and is cached for 15 seconds, failing closed. Example successful launch settings:
{
"enabled": true,
"economicsVerified": true,
"revenueBasis": "assumed-net-ad-revenue",
"revenueMicrousdPerAd": 1000,
"measuredCostMicrousd": 4000,
"minimumMarginBps": 2000,
"creditsPerMessage": 25,
"dailyBudgetMicrousd": 100000
}
All cost/revenue settings are integer USD millionths. AWS's us-east-1 catalog, published 2026-10-29, lists standard Mistral Large 3 input at USD 0.50/million and output at USD 1.50/million. The 4,000/1,000 token ceilings cost at most USD 0.00350 for inference. The USD 0.00400 variable-cost reservation includes a serving allowance and must also be checked against measured Lambda, API, DynamoDB, polling, dispatch, SSM and log costs; it is not invoiced billing.
Launch uses max(4000, ceil(2 × observed beta serving cost + 100)) microUSD per turn. At the approved assumed $1 net publisher eCPM, five credits/ad and a 20% minimum variable margin, the required price is ceil(cost × 5 / (1000 × 0.8)): 25 credits at the floor. Prices above 25 block activation; higher observed costs keep chat disabled. The runtime also enforces this margin and reserves max(4000, measuredCostMicrousd) atomically.
This is an approved revenue assumption, not verified realized ad revenue or overall business profit. Reconcile actual net revenue per reward-granting impression, fixed/shared hosting/recovery, free signup/admin credits and acquisition costs in the financial forecast. The global admission cap includes free-credit turns but does not bound all existing fixed/shared app costs.
Non-production tests may temporarily allow at most five specific user UUIDs for up to one hour. The paid integration suite uses a synthetic user, 20-minute expiry and USD 0.10 daily stage reservation cap, restores prior settings, and never claims illustrative revenue is verified. Spent reservations remain intact across preflight and main release validation. Production ignores this override.
Evidence, rollout and rollback
IDs/statuses, credit charges, token/latency and consumed DynamoDB capacity are recorded without messages, expense content or raw provider output. Evaluation observes native response usage before financial/schema parsing, so rejected responses still contribute their reported model cost. Structural diagnostics expose only fixed field names, types/counts/lengths, known finish reasons and sanitized contract paths/codes.
Live acceptance requires all 17 cases / 20 provider calls, including one-person everyone/self splits, split and missing-FX clarification, three-turn amount/date correction, second-roster caller identity, multiple payers/FX, discounts, percentages/shares, JPY/BHD precision and French/Japanese text. Duplicate normalized names must pass a separate preflight rejection with zero calls/tokens; unknown payers/off-topic input must produce no draft. Exact per-case counts and the preflight reason are required at launch. Each model suite reserves at most USD 0.10, with USD 0.00350 reserved before each paid call; no automatic paid retry occurs.
Reports stay in the private production desktop-updates bucket under store-submissions/expense-chat/run-{run}-{attempt}/{stage}/, covered by the seven-day lifecycle and CloudFront deny. A checksum manifest is written last and binds run/attempt, commit and stage. Activation requires complete matching beta model/paid-API/serving evidence no more than 24 hours old. Reports are not public downloads or Actions artifacts.
Main Pipeline verifies the repo, desktop startup and required ARM native build, deploys dev/beta with general API/browser tests plus paid chat/credit/save/message-cap/zero-balance checks, then deploys/tests production. Activation calculates the conservative price, checks the authenticated production policy, and runs one real paid chat with a synthetic account at the actual price. The production smoke verifies concurrent duplicate sends charge once, duplicate saves persist one expense, and reads that expense back; it never changes production economics or validation allowlists. Failure disables chat and blocks client publishing; the final rollout guard also rejects skipped required work. Turn enabled off to stop new admissions without deleting recoverable history or blocking manual review/saving.
Native Maestro includes 12-expense-chat.yaml; authored fixtures, phone browser tests and ARM cross-builds do not establish device runtime acceptance. Record an actual device/emulator result separately.