Skip to main content
Glama

Server Details

Personal finance ledger for AI agents — query spending, track bills, forecast cash flow.

Ownership verified
Status
Healthy
Uptime
24.5% over 66 days
Last Tested
Transport
Streamable HTTP · MCP 2025-11-25
URL

TDQS

A4.4/5.0

Scored across 5 tools

Disambiguation4/5

The five tools are largely separable and the descriptions actively delineate overlaps (accounts = dashboard widget, admin bank_connection list = text status, query account_summary = numbers; query reads vs admin writes vs annotate classifications). Some residual ambiguity remains because admin (16 entity families) and query (20 modes) are both broad catch-alls whose boundaries are only clear after reading long cross-references, and paycheck_plan overlaps conceptually with query forecast/recurring.

Naming Consistency3/5

Names mix conventions: noun-ish catch-alls (query, admin, accounts), a verb (annotate), and a snake_case compound (paycheck_plan). Each is readable and memorable, but there is no predictable verb_noun pattern and the two mega-tools (admin vs query) don't signal their write/read split in the name.

Tool Count4/5

Five top-level tools is a reasonable, well-scoped count for a personal-finance domain and avoids tool sprawl. However, the count understates the real surface: admin bundles ~16 entity families and query bundles ~20 modes, so each tool is effectively a large internal menu rather than a focused operation.

Completeness5/5

Coverage is exceptionally thorough: CRUD for accounts, parties, tags, rules, plans, adjustments, documents, imports, positions, positions events and transfer links, plus transaction classification, reporting, forecasting, tax prep and document tie-out. Import workflow (upload staging, dry-run, download) and lifecycle operations like versioning/supersede and undo are present, leaving no obvious dead ends.

Available Tools

5 tools
accountsAccounts dashboardA
Read-onlyIdempotent
Inspect

Show the user's connected banks and accounts as an interactive dashboard widget. Renders a card per institution with sync status, last-sync timestamp, accounts, and one-click actions (Sync, Reconnect, Disconnect, Connect new). Use whenever the user asks to see, manage, sync, reconnect, or disconnect their bank connections. The widget calls back into 'admin' for sync/disconnect and into the web app for reconnect/connect-new (Plaid Link cannot run inside the MCP iframe). Not for a text answer or numbers: connection status in text is admin { entity: "bank_connection", action: "list" }, and balances or per-account activity come from query { account_summary: true }.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint, idempotentHint and closed-world, and the description adds genuinely new context beyond them: the widget is interactive, its Sync/Disconnect actions call back into 'admin', reconnect/connect-new hand off to the web app, and Plaid Link cannot run inside the MCP iframe. This delegation detail explains why the tool itself remains a read.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences, front-loaded with what renders before routing rules. Every clause carries information: rendering contents, trigger conditions, callback targets, and the iframe constraint.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no parameters and no output schema, the description must carry the full burden of what to expect and when to use it, and it does: it describes the rendered widget, its interactive actions, and where mutating behavior is delegated. Nothing an agent needs to select or invoke it is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has zero parameters, so the baseline is 4 with no param semantics needed. The description correctly implies the widget is invoked without arguments.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ('Show the user's connected banks and accounts as an interactive dashboard widget') and enumerates exactly what renders (institution cards, sync status, last-sync timestamp, accounts, actions). It is unmistakably distinct from the sibling tools admin and query.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('whenever the user asks to see, manage, sync, reconnect, or disconnect') and explicit when-not-to-use, routing text status to admin { entity: 'bank_connection', action: 'list' } and balances/activity to query { account_summary: true }. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

adminManage Cashflow
Destructive
Inspect

Manage the household's reference data, plans, documents, and bank connections — the CRUD side of cashflow. 16 entity families, each with its own action set; an unsupported (entity, action) pair is rejected with the list of what that entity does support.

ENTITY MAP: category→global taxonomy, READ-ONLY (list, plus this household's own tags/notes on it) · category_group→its parent groups, list only · party→merchants · tag→transaction tags · account→rename, hide/unhide, tag, note · recurring_series→bills & subscriptions · rule→transaction rules (preview, then create with the returned token) · plan→the annual plan (propose a draft from actuals, then upsert) · adjustment→overlay entries that normalize reported actuals without touching transactions · transfer_link→match two legs of one transfer · document→Paper Trail: file, find, read_text, version, link, tie_out, substantiate, export · import→bank statements (OFX/QFX/QBO + Apple Card, Apple Savings, Rocket Money, Wells Fargo, Monarch CSVs) · bank_connection→list, sync or disconnect bank connections · feedback→file a bug/feature/ux note · position→private holdings (angel checks, fund commitments, advisor equity) · position_event→what moved one (commitment, call, distribution, fee, mark…)

Reads live in the query tool; per-transaction classification lives in annotate. Use admin for the entities themselves.

CATEGORIES ARE A FIXED SET, AND READ-ONLY: the category taxonomy is the same for every user and cannot be changed. { entity: "category" } supports list, add_tag, remove_tag, set_note, remove_note; { entity: "category_group" } supports list only. create, rename, merge and delete are all rejected on both. An unrecognized spending type belongs under the closest existing category — record the nuance with set_note or add_tag, which are user data about a category rather than a change to it. To move transactions BETWEEN categories use annotate { action: "categorize" } or a rule, not a merge. This also applies to annotate: { action: "categorize", category_name } only ever matches an existing category, it never makes one. Parties, tags, accounts, and rules are unaffected — those are still created, renamed, merged and deleted freely. Activate/deactivate rules via action:"update" with active:true|false. RECURRING SERIES DECISIONS last until the user reverses them — detection never undoes them:

  • Declare a bill detection missed: { entity: "recurring_series", action: "create", name, frequency, transaction_ids: [id], preview: true }. Supply a cadence even for one selected charge; omit preview to create it and manually link the selected posted charges. The declaration and cadence persist until explicitly ended/rejected; active:true affirms that intent on an existing series. To link later charges (including on another card), use annotate set_recurring with recurring_series_id; it refreshes estimates without silently reviving an ended series.

  • Not recurring (a false detection — a burst of trips, a habit, not a bill): { entity: "recurring_series", action: "reject", id }. Unlinks it and keeps it out of bills, forecast and missed alerts; its charges stay in spending. delete does the same and keeps the row as the marker, so detection cannot re-create it.

  • Ended (a real bill that stopped): action:"update", active:false. Its past charges stay linked; a new charge after today brings it back.

  • Restore either: action:"update", active:true.

  • rename sets the user's name for the series; detection keeps its own and never overwrites the user's. Renaming to the detected name clears it.

When reporting back to the user, translate to plain English.

LIST CONNECTIONS: { entity: "bank_connection", action: "list" } returns each connected bank with status (ok / syncing / needs_reconnect / warning, plus msg), last_sync and ago (when we last asked for new transactions — this moves even when the bank itself has sent nothing, so it is not when the bank last updated), last_new and new_ago (when the bank last sent new or changed transactions; absent if it never has), next_by (around when it is next checked on its own; absent while a connection error stands), no_update_since (present only when the bank stopped sending updates: since then our checks succeed but get nothing newer, and balances may be out of date — a connection that has stopped updating, not a quiet account; status reads warning, msg says what to do), and its accounts. A not_connected row (no id, last_activity instead of last_sync) is a bank whose accounts no connection feeds any more — its numbers stopped updating, and the fix is connecting that bank again, not a reconnect. Answers "is my bank connected?" and "when did it last get anything new?" in text; the accounts tool shows the same as a dashboard.

SYNC: { entity: "bank_connection", action: "sync" } checks every active bank connection now (pass id for one). Rarely needed: new transactions arrive automatically when the bank posts them, and every connection is re-checked at least daily, so sync only when the user reports a specific missing transaction — never on a schedule or before every read. A sync cannot bring in anything the bank has not released yet. Returns added/modified/removed counts per connection with the same status/last_sync/last_new/next_by/no_update_since as list, and a note — a sync that found nothing says when the bank last sent something new, or that the bank has stopped sending updates.

DISCONNECT: { entity: "bank_connection", action: "disconnect", id: "" } revokes the bank connection at Plaid and removes it locally. Returns { ok: true, institution } on success.

FEEDBACK: { entity: "feedback", action: "create", type: "bug"|"feature"|"ux", name: "title", description: "details", tool: "query"|"annotate"|"admin", context: "JSON" }. type and name (the title, max 200 chars) are both REQUIRED; description is capped at 2000. Fire-and-forget — no list/update/delete. Rate limited to 30/day.

IMPORT: { entity: "import", action: "import", file_content: "" } imports a bank export. The format is auto-detected from the content — OFX/QFX/QBO from any US-dollar account, plus Apple Card, Apple Savings, Rocket Money, Monarch and Wells Fargo checking CSVs; anything else is rejected with detected_format. (csv_content is the same field under its older name.) Pass dry_run: true first to preview without writing — it reports valid/invalid/duplicate rows and sample mapped rows. Re-importing overlapping data is de-duplicated.

  • Apple Card is SINGLE-account: by default transactions land in an "Apple Card" account created on first import; the response returns account_id. Pass account: "" to target a specific manual account instead (connected/bank-synced accounts are rejected). The preview also reports how much will move from spending to transfers; importing the card moves your Apple Card payments (made from checking) from spending to transfers so they don't double-count against the card's purchases.

  • Apple Savings is SINGLE-account: by default transactions land in an "Apple Savings" account created on first import — the same account an Apple Savings OFX/QFX/QBO import uses, so importing both exports of a month doesn't double-count. Pass account: "" to target another manual account. Interest is income, Daily Cash deposits are card rewards, and ACH moves to or from a linked bank are transfers; any other activity type imports uncategorized and is named in unmapped_activity_types.

  • Rocket Money is MULTI-account: a single file carries many accounts (each row names its account/institution/type/number), and accounts are matched or created automatically — the account override does NOT apply. Transfers and ignored rows are taken from the file. Accounts already connected via Plaid are SKIPPED (importing a CSV there would double-count synced data) and reported under skipped_connected_accounts / warnings. The response returns accounts[] (name/status/account_id), accounts_created, imported, and skipped_duplicates.

  • OFX/QFX/QBO are ONE format in three extensions and are the BEST export to use where a bank offers it: each transaction carries the issuer's own stable id, so re-importing the same file (or the .qfx after the .ofx) updates those rows instead of duplicating them, and a month imported from OFX reconciles with the same month imported from CSV — neither double-counts the other. Accounts resolve from the file's own institution, so account is optional for a single-statement file and rejected for a multi-statement one. Prefer .ofx over .qfx/.qbo: the Intuit variants cut merchant names off at 32 characters, and the import warns when it sees that.

  • Wells Fargo checking is SINGLE-account and the file omits account identity: pass account:"". A Plaid-connected target also requires allow_connected:true; exact overlapping observations load into Silver and are linked/hidden, while statement-only history remains visible. STATUS=Pending is preserved.

UPLOAD (avoid pasting big files): instead of inlining csv_content, stage the file out-of-band so its bytes never go through context. { entity: "import", action: "request_upload", filename: ".ofx|.csv" } returns a one-time upload_url + a ready-to-run curl (PUT the file from your sandbox: curl -X PUT -T ); the PUT responds with an upload_id. Then import by reference: { entity: "import", action: "import", upload_id: "", dry_run: true } to preview, then without dry_run to commit. Provide exactly one of file_content and upload_id. The upload link is short-lived and self-authenticating (no token needed for the PUT). Prefer this for anything non-trivial — pasting a file inline truncates and burns context. DOWNLOAD ORIGINAL: { entity: "import", action: "get", id: "", with_download: true } returns source metadata plus download {download_url, method, headers, expires_at, filename, content_type, byte_count, sha256}. The native source id is the upload_id returned by staging. It reads the owner's stored CSV/OFX original, never an export reconstructed from transactions. A source whose bytes were not retained reports that limitation. Without with_download, get returns metadata only. The host GETs the bytes without redirects/cookies, verifies actual byte count and SHA-256, then publishes privately to a caller-chosen local path without overwriting. Keep bytes/base64 out of context; on expiry request new instructions for the SAME id. Paper Trail { entity: "document", action: "get", id, with_download: true } supplies the same descriptor and retains its legacy download_url/download_expires_in fields. UPLOAD TRANSFER (imports and documents): filename is the only required file metadata for preparation; no caller-generated IDs, hashes or exact sizes are needed. The host reads the original file, sends raw bytes using the returned method/headers within max_size_bytes, and mechanically compares the receipt's sha256 AND byte_count with its local original before using the native upload_id. Server hashing alone does not verify the local original. On failure or a mismatched receipt, keep the instructions for recovery; do not silently prepare anew or automatically retry.

VESTED INTEREST: what the household owns that has not resolved yet — angel checks, fund commitments, advisor equity — as EVENT LOGS, so cost basis, unfunded and DPI are folds over dated events rather than typed numbers. Read it with query { vested_interest: true }. Field-level rules live on the position, positions, event, events and statement_check params.

  • Position: { entity:"position", action:"create", position: { name, issuer, holder, … } }, or positions:[…] (up to 100) for a batch. issuer, holder and vehicle are declared, never resolved to a party; one company held through two vehicles is two positions under one issuer. Declare segment rather than guessing it. strike/expiry belong to option / warrant / advisor_equity only; changing the instrument under a stored strike is refused unless both are cleared with null in the same call. update by id (null clears a field) answers with the row's identity plus only the fields THAT CALL set, read back as stored — everything untouched is one get away. get (position + events + folded figures), list (include_resolved:true for exited ones; the first page adds unused: { issuers?, holders?, vehicles? } — registry rows no position uses), delete (removes its events and document links; its issuer, holder and vehicle rows are KEPT and reused by name, and the reply's kept says how many positions each still has).

  • Terms: record each term WITH its provenance — basis document (+ document_id and page), assumed, or missing — in the source's own wording, never parsed. Then query { vested_interest, terms_basis:["assumed","missing"] } IS the what-only-you-can-answer list; do not keep that in a note. A term you looked for and could not find is basis "missing", not a remove_terms.

  • Event: { entity:"position_event", action:"create", event: { position_id, kind, amount, effective_date, … } }. amount >= 0. A CAPITAL CALL has three states: the commitment; the NOTICE (kind contribution, status:"pending", effective_date = due date — it shows in the forecast and as past_due, not in paid-in); the WIRE (status settled, supersedes_id = the notice, transaction_id = the bank row), which marks the notice superseded. A MARK is an opinion: mark_source is required and it is never summed — record every statement's NAV as its own mark so a stale one can be seen. Two marks on one as-of date: the one recorded in the LATER CALL wins, so record competing same-day opinions in separate calls, not one batch. A resolved position's last mark is its final word and never goes stale. Every event row carries recorded (when it was entered).

  • Event list: recorded_since (ISO instant with timezone, at most milliseconds) finds entries recorded since then, inclusively, even when backdated. It returns terse rows in recorded/id descending order within a fixed first-page time window. Keep recorded_since, position_id and pending_only while following cursor; save as_of ONLY from the final page, then use it for the next poll and deduplicate boundary ids. Ordinary lists remain in effective-date order. start/end are refused. This reads current rows by creation time: edits/deletes are not history, pending_only uses current status, and a write timestamped earlier but committed after the traversal can be missed; this is not a commit-ordered change log.

  • Statement batch: { events:[…], position_id?, statement_check } — refused with identity_mismatch when the statement's own arithmetic does not tie, and with book_mismatch when its contributions or distributions disagree with the book. Fees are never compared (the book's fee kind is out-of-pocket only). The response echoes { scope, scope_source: "declared"|"inferred", book: "ok"|"not_compared" }. statement_check is a gate, not a record — only the events and their verbatim lines are stored.

  • transaction_id / document_id must be this household's own rows; a foreign id is refused by name. Currency is per position and totals never cross currencies.

DISCOVERY: { entity: "category"|"party"|"tag"|..., action: "list" } returns reference data for the user. All entity types support list. PARTY LIST: Supports search, limit (default 50), and cursor for pagination. Response includes cursor/has_more when more results exist — pass cursor to fetch the next page. The same paging applies to every list in party, rule, tag, recurring_series, position, position_event, adjustment, document (position returns n for the page and matched for the total); category, category_group, account, plan, bank_connection always return the full set.

LIST USAGE: Category and party rows include n, the number of posted, nonignored transactions with that effective category/party in period (default this_month; also last_month, this_year, all). This follows household report visibility: hidden accounts are excluded unless include_hidden:true, while inactive account history remains. Each transaction counts once, including transfers, refunds and zero amounts; adjustments do not add transactions. Categories remain the full taxonomy with n:0 when unused. Party membership and eff/auto counts remain lifetime diagnostics, so a listed party can have n:0. Tag n and src provenance are also lifetime, unaffected by period; adjustment_n separately counts tagged adjustments (all statuses).

RECURRING LIST: next agrees with query { recurring:true }: for active, recently seen bills, it is the next date on or after today; expected carries the original date when next rolls forward. Cancelled bills keep their last expected date. Active bills that stopped charging keep that date too, flagged stale:true; never-seen bills are also stale.

PARTY DUPLICATE REVIEW: query { detail:true, tag:"dup-candidate" } surfaces sample transactions from party-name clusters the diagnostic clusterer (query { clusters:true }) flagged as likely one merchant split across spelling variants.

  • Resolve via merge: { entity:"party", action:"merge_cluster", merge_into_id:"", member_ids:["", ...] } folds every member into the target in one atomic call — re-points enrichments + plan references, clears the dup-candidate tag on affected transactions, deletes the member party rows. { action:"merge", id, merge_into_id } is the single-pair form (same underlying merge). Both responses include suggest_rule when the merged names share a literal prefix: { description_pattern, party_name, reason } — pass description_pattern straight into the RULE WORKFLOW's preview step when it's present. A plain merge only fixes what already exists; a rule also prevents the same merchant from re-fragmenting on the next import, so treat suggest_rule as a prompt to consider creating one, especially for an established/recurring merchant (a one-off historical cluster not worth a permanent rule is fine as a bare merge).

  • Dismiss a false positive: { entity: "annotate" (tool), action:"untag", tag_name:"dup-candidate" } on the flagged transaction — writes a suppression tombstone so the same cluster isn't re-flagged.

RULE LIST: Supports search (matches name, description_pattern, or party_pattern), limit (default 50), and cursor for pagination. Response includes cursor/has_more when more results exist. Each rule reports wins (how many transactions it decides now). include_matches:true adds matches (how many transactions its conditions match now — the number rule preview's total gives, counting ignored rows and rows hidden as duplicates too) and last_match (the newest one's date); both are counted fresh from the transactions as they are, including each one's current merchant, so re-applying never changes them and a rule that renames the merchant it keys on can win rows it no longer matches. include_matches reads every transaction: pair it with search or a small limit. RULE WORKFLOW: preview → create/update (with token). A create OR update WITHOUT a token writes nothing and is REJECTED — never silently previewed (so an "add set_party" edit can't quietly no-op). Create auto-applies the rule.

  • Preview (action:"preview", or preview:true on create/update) returns: { preview:true, total, matched, token, candidate_fields?, manual_matches, txns: [...], conflicts?: [{ id, name, candidate_wins, existing_wins, existing_fields?, shared_fields?, disjoint? }] }. candidate_wins counts the PROPOSED rule's wins; existing_wins counts the NAMED existing rule's wins. Legacy wins/loses mean candidate_wins/existing_wins. Each txn may have conflict (existing rule name) and conflict_winner ("candidate"|"existing"); legacy conflict_outcome ("wins"|"loses") is candidate-relative. One whole rule wins by specificity (longer matched pattern), then oldest rule/id on a tie: different fields do NOT combine. candidate_fields and existing_fields list category, party, is_transfer/is_ignored/is_flagged, or metadata: (tag add/remove both use metadata:tag). disjoint:true means no shared action field, NOT protection from the winning rule. Update fields include retained actions. manual_matches counts all manually edited matches, including off-page; txn.manual:true marks them. Applying skips them unless overwrite_manual:true, which can replace their manual names/categories. total counts matches, not promised writes. txn.raw is the bank's original text (what description_pattern matches); name_in is the source's shorter description, also tested (absent when the same as raw). ign:true marks an ignored transaction; it still counts in total.

  • Create requires: token from preview + at least one condition + at least one action. name is optional — left off, it defaults to what the rule matches and does ("eBay → Other Income"). Actions: set_category_name, set_party_name, set_is_transfer, set_is_ignored, set_is_flagged, set_tag_name, remove_tag_name (strips a system auto/rule tag from matches; not a user's manual tag; mutually exclusive with set_tag_name). Returns { created, applied }. The token binds the previewed conditions AND actions — change either and the write is rejected, so preview with the same actions you intend to apply.

  • TRANSFER FLAG: When applied, set_is_transfer:true marks transactions as transfers; set_is_transfer:false clears the flag and overrides automatic transfer classification on future syncs. To recategorize an automatic transfer, preview with set_category_name and set_is_transfer:false together.

  • PATTERN CHOICE: description_pattern (if_desc) matches the cleaned description AND falls back to the raw bank memo — raw-anchored, survives Plaid merchant churn. party_pattern (if_party) matches ONLY the derived party name, which Plaid can rename on a resync, silently breaking the rule (#1211). For consolidation/renaming, prefer if_desc; a create/update targeting an entity-backed party returns a warning. query { health:true } → stale_party_rules flags a party rule whose merchant kept showing up after the rule's last win without the rule matching it (missed + missed_example show those charges), or that matches no transactions. A merchant that simply went quiet is not flagged.

  • BEFORE creating, search existing rules (action:"list", search:"") for the same pattern. If one exists, UPDATE it (action:"update", id) instead of creating a parallel rule — duplicate/overlapping rules pile up and the older one silently wins. A create with conditions identical to an existing active rule is refused unless confirm_duplicate:true; a create for a merchant that already has a rule still succeeds but returns a warning.

  • AUDIT: { entity:"rule", action:"list", audit:true } returns groups (same-merchant rules; exact:true = identical conditions, exact:false = differing) and shadowed rules (their conditions match transactions but they decide none — a more specific rule or a manual edit takes every one). Run it to find redundancy.

  • Update: { entity: "rule", action: "update", id: "", ...changes }. Same round-trip as create (preview:true, then re-send WITH the token); the token binds the merged conditions (existing + updates) AND the actions. active:true|false needs no token — one call that re-applies or clears the rule's effects.

ACCOUNT VISIBILITY: { entity: "account", action: "hide", id: "" } hides an account from queries and listings. "unhide" reverses it. List with include_hidden:true to see hidden accounts (flagged hidden:true). An account in another currency than the household's is listed with status:"not_counted", a notice to relay, and its own ccy: it is shown, never summed.

TAGS: { entity: "account"|"category", action: "add_tag"|"remove_tag", id, tag } manages tags on accounts and categories. Tag is lowercased and hyphen-joined (e.g. "Emergency Fund" → "emergency-fund"). Canon tags for accounts: operating, bills, emergency-fund, savings-goal, brokerage, retirement. Canon for categories: essential, discretionary. Custom tags are allowed; non-canon adds return is_canon_tag:false. List responses on account/category include tags: string[].

NOTES: { entity: "account"|"category", action: "set_note"|"remove_note", id, note } manages a free-form text note on each account or category. Single note per entity — set_note replaces any existing note (UPSERT). Empty/whitespace notes are rejected. List responses on account/category include note: string when present. Useful for context like "joint account, both partners contribute" or "Software category includes hosting + dev tools".

PLAN: the household's annual financial plan — one declarative document per year (income items, tag-bound events, ONE everyday-base number, guardrails, exclusions). Amounts in the household's currency; category/group/party referenced by id; tags by name. Tags in the document (events, matches, guardrail scopes, exclusions) reference TRANSACTION tags; guardrail scopes and exclusions may instead use { category_tag: "..." } to target CATEGORY tags (the essential/discretionary canon) — { tag } and { category_tag } are different namespaces. Plans are keyed by year — name is rejected (year defaults to the current year). Upsert rejects a guardrail/exclusion scope naming the other namespace (a { tag } that is only a category tag, or vice versa); it warns (not errors) on other tags matching nothing and on events whose bindings overlap.

  • Propose (draft from actuals): { entity: "plan", action: "propose", year? } → { document, basis, notes } — a draft plan built from the household's own last-12-months actuals (everyday base from trailing expenses, income from recurring deposits, events from annual/semiannual bills, guardrails on the top groups at last year's levels). Read-only: nothing is written. Review + edit the returned document, then pass it to upsert. Year defaults to NEXT year. Use this instead of authoring a plan from a blank page.

  • Upsert: { entity: "plan", action: "upsert", document: {...} }. First upsert for a year writes the plan as written; upserting over an existing plan REQUIRES reason and records a dated revision (the original is never edited). The server computes the seasonal base shape from the prior year's history (falls back to flat with shape_note when history is thin). Get output is valid upsert input (one exception: a stored scope naming the other tag namespace is rejected until fixed in that revise): server-computed base fields (shape, shape_source, shape_note) are accepted and ignored on write — recomputed every upsert.

  • Get: { entity: "plan", action: "get", year? } → latest document. revision: 1|"original" for the plan as written; "diff" for original-vs-latest structural diff + reason trail. No plan for that year → { error, hint } pointing at propose.

  • List: { entity: "plan", action: "list" } → one row per year with revision count.

  • Resolve an exception: revise the plan (upsert with reason) so the inputs change and the breached exception evaporates on the next plan_status read. Open exception keys are listed in query { plan_status: true } exceptions[].

  • Delete: { entity: "plan", action: "delete", year } — removes the whole plan year (all revisions); reports the revision count. Requires an explicit year (or id); for mistakes, not for revising.

  • Status reads live in the query tool: query { plan_status: true }.

ADJUSTMENT: an overlay that normalizes reported actuals WITHOUT editing real transactions — actuals stay the immutable "as-booked" truth; adjustments fold in only when a report asks for the adjusted view. CRUD via { entity: "adjustment", action: "list"|"create"|"update"|"delete", adjustment: {...} } — the payload goes under the "adjustment" key (NOT "document", that's for plans). Amounts in the household's currency; periods as YYYY | YYYY-MM | YYYY-MM-DD; free-text label is "note" (aliases: memo, rationale). TARGET A CATEGORY (category_id OR category_name; aliases category / group / group_name) — this is what makes a reclass/imputation show up in the category breakdown (top_cats) and in grouped by:category; without it only the headline total moves. Three types:

  • accrual (period-shift): { type: "accrual", direction, amount, source_period, target_period, category_name? } — moves a flow from source_period to target_period (e.g. a January bonus earned the prior year). Nets to zero across a range spanning both.

  • imputation: { type: "imputation", direction: "outflow", amount, target_period, category_name?, funding_account_id? } — books a real off-feed expense (in category_name, e.g. "Property Tax") plus an offsetting investment withdrawal. funding_account_id is OPTIONAL (validated if given; the withdrawal leg books regardless), so off-feed sources you don't track need no stand-in account.

  • reclass (alias "exclude"): { type: "reclass", direction, amount, target_period, category_name? } — excludes/removes a one-off from the series (e.g. an estimated-tax check distorting operating spend). Set category_name so it reconciles in the category breakdown.

  • Preview before writing: pass dry_run:true to create — returns the computed legs + net effect without inserting.

  • Tags: adjustment:{tags:["trip-slug"]} sets household transaction tags on create/update (full replacement; omit preserves; [] clears). List/create/update return tags. Tags apply to both primary and funding legs; type:"expense" selects the expense in grouped reports, while summary reconciliation retains the investment offset. Tag names share admin tag list/rename/delete; tag n counts bank transactions and adjustment_n counts reporting entries. A YYYY period remains year-only, never inferred from trip dates. Retire the imputation if the real payment is later imported to avoid counting twice.

  • Soft-retire with update { id, adjustment: { status: "retired" } } to drop it from adjusted reports while keeping the audit trail; only status="applied" entries fold in.

  • See the result in the query tool: query { view: "adjusted" } (alias "actual" for as_booked) changes reporting totals (and their prior-period comparisons); query { reconcile: true } returns the as-booked↔adjusted bridge (summary + time-series).

  • Real expense paid outside connected bank feeds: { entity:"adjustment", action:"create", dry_run:true, adjustment:{"type":"imputation","direction":"outflow","amount":1200,"target_period":"2022-08-15","category_name":"Hotels","tags":["summer-trip-2022"],"note":"Lodging paid outside connected bank feeds"} }. Check list for an existing entry first, review the preview, then omit dry_run to save. This adds $1,200 of Hotels spending and an equal investment withdrawal; overall net and original bank transactions stay unchanged. Query the same dates with view:"adjusted", reconcile:true, or by:["category"], type:"expense" for the spending breakdown.

  • Use imputation only when investment-withdrawal funding matches the payment. Adjusted reports support summary, time series, and grouped reports with exactly one group/category/tag dimension and type other than pnl. Category/group filters select each leg separately; tag/exclude_tag select the entry's tags on both its expense and funding legs. Party, account and other unsupported transaction filters disable the overlay: check view and param_warnings for as_booked fallback. Tags overlap: never sum tag rows. tag_report and detail remain bank-only; tag child breakdowns and matrices keep a reporting-entry remainder. Adjustments create no bank transactions. Coarse YYYY or YYYY-MM periods count in full on any overlap, without proration, and land in the first overlapping trend bucket; use an exact YYYY-MM-DD payment date when known. Retire the imputation if the real payment is later imported to avoid counting it twice.

DOCUMENT (Paper Trail): the household's permanent document store — statements, receipts, invoices, forms, policies and contracts, filed against the transactions and accounts they explain. Params are FLAT (doc_type, title, issuer, ...), NOT nested under a "document" key — that key belongs to plans.

  • FILING IS TWO CALLS, and the bytes never go through context. { entity: "document", action: "request_upload", filename: "receipt.pdf" } returns upload_url + a ready-to-run curl (PUT the file from your sandbox); the PUT responds with an upload_id. Then { action: "create", upload_id, doc_type, title, issuer, period_start, period_end, tax_year, link_transaction_ids, link_account_ids, link_position_ids, provenance }. provenance records where you got it (a Gmail thread id, a Drive file id, a portal URL) so the trail survives the conversation. There is no inline-content option: never paste a PDF into a tool call.

  • FILING MANY IS STILL TWO CALLS. { action: "request_upload", files: ["q1.pdf", "q2.pdf", ...] } mints a slot per file at once (up to 25); the response supplies uploads[] {filename, upload_url} and one curl_template (replace <LOCAL_PATH> and <UPLOAD_URL> per slot); PUT each to its own upload_url; then ONE { action: "create", documents: [{ upload_id, title, ... }, ...] }. Top-level document fields are DEFAULTS every entry inherits and may override — { issuer: "Scribble Ventures", tax_year: 2025, documents: [{ upload_id, title: "Q1 report" }, ...] } — except supersedes_id, which is never inherited. The batch answers with n + documents[] (each carrying its index), and one document still answers with document + download_url. Filing is PER DOCUMENT: a bad entry comes back in errors with its index and does not stop the rest, and re-sending is safe (filing the same bytes twice updates that document rather than duplicating it). Naming one upload_id twice in a batch is refused outright. Do NOT merge separate documents into one PDF to save calls — a statement and a report filed as one blob can no longer be superseded or linked apart.

  • PAPER TRAIL TRANSFER: use the returned binary Content-Type unchanged for PDFs/JPEGs/PNGs; the server recognizes their original bytes and stores the matching MIME. For each batch slot, combine the shared method/headers/limit with that slot's upload_url before transferring; the host routine needs no document-specific header override. Verify each receipt before filing.

  • LINKAGE IS THE PRODUCT, and it is EXPLICIT. You name the transactions, accounts and Vested Interest positions a document is evidence for; nothing is inferred from the description, the party or the amount. A link to a transaction, account or position outside this household is refused, not skipped. File a K-1 with link_position_ids naming the position(s) it reports — a consolidated K-1 package names every position it covers — and query { tax_documents } marks those K-1s received. To link a document already on file, call create again with upload_id set to its id plus the link_position_ids — links are added, never replaced.

  • doc_type is DECLARED, not detected: statement | receipt | invoice | tax_form | policy | contract | disclosure | notice | other. There is no classifier — set it from what the user tells you, and say so rather than guessing confidently.

  • STATEMENT PERIODS ARE READ FOR YOU: filing a doc_type "statement" PDF without period_start/period_end parses the billing cycle out of the document (a closing date + days-in-cycle, a numeric two-date period such as Chase's opening/closing dates, Navy Federal's and Wells Fargo cards' statement period or Citi's billing period, an explicit range, or Wells-Fargo-style beginning/ending balance dates). The response echoes period_source:"extracted" and period_form — CONFIRM the dates with the user rather than assuming them. A period you pass is never overwritten — but if it disagrees with the cycle the document itself declares, the response carries period_extracted + period_note so you can re-file with the right dates (hand-typed periods are the main source of off-by-a-day cycles). A non-PDF or an unknown form just files without one, and tie_out reads the stored period back to decide which ledger rows a statement covers.

  • FIND: { action: "list", account?, start?, end?, doc_type?, tax_year?, issuer?, search?, search_scope?, text_status?, link_transaction_ids?, link_position_ids?, include_superseded?, limit?, cursor? }. Returns n (this page), total (all matching documents), has_more and cursor when more remain; default 50, max 500. Continue with cursor and the same filters. "search" matches what the filer typed (title, issuer, filename, note, provenance) AND the documents' own text — "which fund reports mention Whatnot" is one call. A body hit carries text_match {page_n, pages, lines}: how many pages contain the term, the first few, and a line of context; read_text with find reads the rest. search_scope:"metadata"|"text" narrows it (default "all"). When some documents' bodies could not be searched the response says so — text_queued (awaiting indexing; body search may miss them, though requeued files can retain old searchable pages) and text_unsearchable (scans and photos with no text layer) — so a miss is not proof of absence. Indexing runs hourly in bounded batches, with no completion estimate. List with text_status:"queued" and the same archive filters, omitting search, to identify waiting documents. text_status also accepts indexed, partial, no_text, not_text, unreadable, no_bytes, corrupt, failed (list/export ONLY). Each row shows filed_age_days: whole elapsed days since filing, NOT queue residence; omitted when filing time is unknown. Dates match by PERIOD OVERLAP (a March statement matches a search for 15 March); a document filed with no period_start is not date-searchable. { action: "get", id } adds a short-lived download_url.

  • READ WHAT IT SAYS: { action: "read_text", id } returns the document's text — the numbers inside a K-1, a statement's balances — without downloading anything. PDFs are read in reading order; a stored text file comes back verbatim. Optional from_page/to_page (1-based, inclusive; 40-page cap) and max_chars (default 50,000; the response says truncated:true when it bit — narrow the pages rather than raising it). Optional find:"term" returns ONLY the pages containing it (case-insensitive), with match_pages and the matching lines — the way to check one sentence in a 13-page FAQ without reading it all (match_n:0 when it is not there). A scan or a photo is refused BY NAME as no_text/not_text: those need OCR, which only the reconcile CLI can run. A window past the end refuses as out_of_range with the document's page count.

  • VERSIONS: pass supersedes_id on create for a correction or re-issue. The old edition is marked superseded (hidden from list unless include_superseded) and its links move to the new one — except links whose target no longer exists, which the response lists as dropped_links.

  • RELINK AFTER FILING: { action: "link", id, link_transaction_ids?, link_account_ids?, link_position_ids? } adds links to a filed document (targets checked against this household); { action: "unlink", id, link_*_ids } removes exactly those links, including ones whose target no longer exists, and refuses a link the document does not hold. To re-point a link, link the new target first, then unlink the old one. query { health: true } lists the links that need it under orphaned_edits.

  • TIE_OUT: { action: "tie_out", id } ties a filed statement's OWN transaction table out against the ledger, row by row, one report per linked account. The workflow: file the statement (linked to its account via link_account_ids), then tie_out by document id — matched counts the rows with ledger twins, unexplained lists statement rows with NO ledger twin (candidate ledger gaps — the thing worth investigating), ledger_only lists ledger rows the statement doesn't show, explained counts rows whose only twin the ledger deliberately hides (dedup losers). verdict is "clean", "review" for weak/hidden/summed evidence, or "discrepancies". This all-source comparison can include reconstructed rows and does not establish independent Plaid coverage. Add match_trace:true (limit default50/max500) for bounded transient row/page references, ledger ids, tiers/date deltas/ambiguity and rollup diagnostics tied to fingerprints, with separate imported-book and Plaid results. No Plaid account/feed observations means unavailable coverage, not proof of missing imported transactions. Traces omit amounts/memos and are not saved; total/truncated name omitted rows. It compares rows, not balances. Read-only. A scanned statement with no text layer is refused by name (no_text) — the reconcile CLI is the OCR-capable door for those.

  • SUBSTANTIATE: { action: "substantiate", account?, start?, end? } asks which charges have a document filed as evidence — vouching, from the charge out to its support. Returns is_onto (every charge has at least one document), is_one_to_one (nothing carries more than one of the other), is_bijection (both, plus no unattached documents), plus without_document — the charges nothing explains.

  • EXPORT: { action: "export" } returns a page of the archive MANIFEST — filed documents with their links, hash and byte size, plus n (this page), total (all matches), has_more and cursor when more remain. Default 50, max 500; continue with cursor and the same filters. Always available, and it takes the same filters as list (doc_type, tax_year, issuer, search, search_scope, text_status, account, start/end, link_transaction_ids, link_position_ids, include_superseded, limit, cursor). Bytes are not included and download_urls are NOT emitted by default: pass with_urls:true if you need them, knowing each one is a live bearer token and the whole payload lands in the transcript. For one document use { action: "get", id }; for a real offline archive run the export-documents CLI.

  • RECONSTRUCT: { action: "reconstruct", id } turns a filed statement's own transaction table into ledger rows — the tool for a period no feed covers (a dead bank connection, history older than Plaid reaches). DRY-RUN BY DEFAULT: it reports what it would insert and writes nothing until dry_run:false. It refuses when the statement's cycle already holds visible rows for that account, because a statement's descriptors do not reliably dedup against an aggregator's — "force" overrides that and WILL insert duplicates by design, so the response carries a batch_id for "undo-import --batch-id" to roll back. Every section must pass the statement's own printed arithmetic before anything is written. Use "section" to pick one account out of a combined statement when the ledger account has no mask.

  • DELETE: { action: "delete", id } is real — the document and its links go at once. Its stored file is kept (Cashflow never deletes stored files), so filing the same file again undoes the delete.

  • RELATED: query { tax_documents: true } PREDICTS which tax forms a household should expect from its income signals, and now attaches what is on file — each expected row carries filed when a document filed for that tax year NAMES that form, plus also_filed for the ones no prediction claimed, plus a k1 checklist — the K-1s the partnership interests in Vested Interest imply for the year, each received / missing / not_due. So the query mode answers BOTH "what am I still waiting for" and "what do I have" for tax forms; use this entity to file, version, download, tie out or substantiate any document, tax form or not. Filing with an accurate title and tax_year is what lets the prediction find it.

TRANSFER_LINK: the matched-pair overlay for internal transfers. The matcher auto-pairs the two legs of a move between your own accounts (outflow from one, inflow to another) so they cancel in the Transfers line; this surface reviews and corrects those pairs. { entity: "transfer_link", action: "list", link_status?: "suggested"|"confirmed"|"rejected" } lists links (from/to account, amt, dt, status). { action: "confirm", transfer_link_id } promotes an ambiguous "suggested" pair to confirmed and flags both legs as a transfer. { action: "reject", transfer_link_id } marks a false pair rejected and undoes any flagging — rejects are STICKY (the matcher never re-suggests them). { action: "link", outflow_txn_id, inflow_txn_id } manually pairs two transactions the matcher missed (must be opposite directions, different accounts, equal amounts).

ParametersJSON Schema
NameRequiredDescriptionDefault
idNoEntity ID. A UUID for party/account/rule/recurring_series; the global SLUG for category (e.g. "food/coffee") and category_group (e.g. "food") — list them via admin { entity: 'category', action: 'list' }.
endNoDocument list/substantiate: end of the date range (YYYY-MM-DD).
tagNoTag value for add_tag/remove_tag. Lowercased and hyphen-joined; reuses canon tags when possible (essential, discretionary on categories; operating, bills, emergency-fund, savings-goal, brokerage, retirement on accounts).
findNoDocument read_text: a term to look for (case-insensitive substring). The response then holds ONLY the pages containing it — match_pages, match_n and the matching lines (up to 50) beside them, match_n:0 when nothing matched — so one sentence can be checked without reading the whole document. Composes with from_page/to_page.
nameNoEntity name (for lookup or create). entity=feedback: this is the REQUIRED title, max 200 characters — a feedback create without it is refused.
noteNoNote text for set_note. Single note per entity; set_note replaces any existing note. Trimmed; empty/whitespace rejected.
toolNoWhich tool triggered this feedback
typeNoFeedback type (for feedback entity)
yearNoPlan year (entity=plan). Defaults to the current year for get/delete.
auditNoRule list: return a duplicate/overlap/shadowed audit instead of the rule list.
eventNoEvent payload for create/update (entity=position_event): { position_id, kind, amount, effective_date, status?, settled_date?, quantity?, entry_valuation?, entry_valuation_basis?, transaction_id?, document_id?, supersedes_id?, mark_source?, mark_basis?, verbatim?, note? }. In the position's currency; kind carries the sign; entry_valuation is the post-money a contribution or conversion came in at.
filesNoDocument request_upload: mint one PUT slot per filename in a single call, instead of one call per file. The response hoists the shared half (method, headers, expiry, limits) and lists {filename, upload_url} per file, plus one curl_template: replace <LOCAL_PATH> and <UPLOAD_URL> for each slot. Then file them all with ONE create + documents:[...].
forceNoDocument reconstruct: write even though the statement's cycle already holds visible rows for the account. UNSAFE — those rows will not reliably dedup against a statement's own descriptors, so this inserts duplicates; the response carries a batch_id for `undo-import --batch-id` to roll back.
limitNoMax results for list and document export (default 50, max 500). Paged (limit + cursor/has_more): party, rule, tag, recurring_series, position, position_event, adjustment, document. Capped by limit, no cursor: transfer_link. Always the full set: category, category_group, account, plan, bank_connection
startNoDocument list/substantiate: start of the date range (YYYY-MM-DD). Documents match by period overlap; a document with no period_start is not date-searchable.
titleNoDocument: a human title ("Chase statement, March 2026").
tokenNoPreview token from a prior preview (action:'preview' or preview:true). REQUIRED to apply a rule create OR update — a write without it is rejected, not silently previewed. The token binds the exact conditions AND actions previewed; changing either invalidates it.
actionYesAction to perform. "reconcile" and "verify" are RETIRED document spellings — call "substantiate" and "tie_out" instead; the old names remain only to return that redirect.
activeNoFor action=update on rules: true reactivates, false deactivates. On recurring_series: false marks a real bill ended (history stays linked), true explicitly affirms the series as recurring until the user ends or rejects it.
cursorNoPagination cursor from a prior page of the same list or document export, with the same filters (paged responses return it with has_more:true)
entityYesEntity type
eventsNoEvent create: a BATCH — one entry per event, e.g. a statement's whole activity. ALL-OR-NOTHING: every reference is validated and statement_check (if given) must tie before anything is written. A top-level position_id is the default for entries that omit their own. Answers with terse rows { id, position_id, kind, status, amt, dt } — the full row is on get/list.
intakeNoDocument: how it arrived. Defaults to "agent"; pass "user" when a person handed it over directly.
issuerNoDocument: who issued it, as free text ("Chase", "Blue Cross"). NOT resolved to a party — issuer-to-party matching is deliberately not attempted. Also filters list (substring).
periodNoPeriod for category/party list usage n (default this_month). Party eff/auto and tag n/provenance remain lifetime counts.this_month
reasonNoWhy the plan is changing — REQUIRED when upserting over an existing plan (creates a dated revision).
searchNoSubstring match. Parties and rules: by name. Documents: across the metadata a filer typed (title, issuer, filename, note, provenance) AND each document's own text, so a portfolio company named on page 10 of a fund report is found. A body hit carries text_match: {page_n, pages, lines} — how many pages contain the term, the first few of them and a line of context; read_text with find reads the rest. Composes with doc_type, issuer, tax_year, start/end and account; superseded editions are skipped unless include_superseded. The response counts what body search may miss: text_queued (awaiting indexing; old pages may remain searchable during reindexing) and text_unsearchable (scans, photos — no text layer). Narrow with search_scope.
statusNoFilter adjustment list by status (entity=adjustment, action=list).
accountNoImport: target account id or exact name for SINGLE-account formats. Optional for Apple Card (defaults to its canonical account) and for a single-statement OFX (which names its own institution); REQUIRED for Wells Fargo because its export omits account identity. Connected targets require allow_connected. Rejected for Rocket Money and for a multi-statement OFX, both of which resolve accounts from the file.
contextNoJSON context: what the agent was doing
dry_runNoImport: validate + report (rows, duplicates, sample mapped rows) without writing anything. Also adjustment create and document reconstruct. Any other write given dry_run:true is refused and writes nothing.
previewNoRule create/update: return matches and a token without writing; token required to apply. recurring_series create: preview the declaration and estimates without writing; omit preview to apply (no token). Other writes given preview:true are refused.
sectionNoDocument reconstruct: which account section of a combined statement to load, by the issuer's printed account number. Needed only when the ledger account has no mask to match on against a statement that does print account numbers (Venmo and PayPal are maskless by construction). Not needed for Apple Card: its statements print no account number at all, and the one verified section of such a statement is picked for a maskless account automatically.
to_pageNoDocument read_text: last page to read, 1-based and inclusive (default: the whole document, capped at 40 pages).
doc_typeNoDocument: what kind of document this is. DECLARED, never inferred — there is no classifier, so this is whatever the filer says it is. Also filters list.
documentNoPlan document for upsert (entity=plan). Amounts in the household's currency; category/group/party/account referenced by id; tags by name.
filenameNorequest_upload: original filename, the only required file metadata. Import extensions pick the transport content type (.ofx/.qfx/.qbo vs .csv); documents use a binary transport type and recognize PDF/JPEG/PNG bytes on receipt. It labels the stored file.
is_regexNoRule: treat description_pattern as regex
new_nameNoNew name (for rename)
positionNoPosition payload for create/update (entity=position): { name, issuer, holder, vehicle?, instrument?, segment?, ticker?, k1?, currency?, status?, funding_account_id?, strike?, expiry?, terms?, note?, holder_kind?, issuer_kind?, beneficial_owner? }. Issuer, holder and vehicle are matched exactly by name within the household or created; strike/expiry are option and warrant terms; segment says which book it belongs to; terms are the typed terms with their provenance, upserted by term (remove_terms deletes one).
revisionNoFor plan get: a revision number, 'original' (revision 1, the plan as written), or 'diff' (original vs latest + reason trail). Default: latest.
tax_yearNoDocument: the tax year this belongs to. Also filters list.
directionNoRule condition: transaction direction
documentsNoDocument create: file a BATCH — one entry per staged upload, each {upload_id, ...metadata}. The top-level document fields (issuer, tax_year, doc_type, …) are DEFAULTS every entry inherits and may override, which is what a single issuer's quarter or a single fund's year actually looks like; supersedes_id is the exception and is never inherited, since it names one specific predecessor. Filing is PER DOCUMENT: a bad entry is reported in errors with its index and does not stop the rest, and re-sending is safe because filing the same bytes twice updates that document rather than duplicating it.
frequencyNoRecurring series create: REQUIRED cadence, even for one selected charge. Detection preserves this declared cadence.
from_pageNoDocument read_text: first page to read, 1-based (default 1). Narrowing the range genuinely skips the work, so it is the right answer to truncated:true.
max_charsNoDocument read_text: character budget for the returned text (default 50,000). The response reports truncated:true when it bit — narrow the page range rather than raising this.
mime_typeNoDocument create: content type of the original, if the upload did not declare one. Metadata only — it is never echoed as a response header.
positionsNoPosition create: a BATCH — one full position payload per entry (a portfolio's worth of syndicate rows in one call). ALL-OR-NOTHING: every entry is validated and every funding account checked before anything is written; a refusal names the entry by index. Issuer / holder / vehicle names repeated across entries resolve to one registry row. Answers with terse rows { id, name, issuer, issuer_id, vehicle?, vehicle_id?, instrument, ccy }.
upload_idNoImport: reference to a file staged via request_upload + the returned PUT URL (keeps bytes out of context). Provide exactly one of csv_content or upload_id. Document: same staging flow; a PUT of already-filed bytes returns already_filed:true with the EXISTING document's id as upload_id, and create with that id updates the document's metadata in place — the sanctioned correction path for a mis-typed period or title.
with_urlsNoDocument export: also mint a short-lived download_url per document. OFF by default — each URL is a live bearer token for the bytes, and a whole-archive payload of them lands verbatim in the conversation transcript. Use `get` for one document, or the export-documents CLI for a real offline archive.
account_idNoRule condition: specific account
adjustmentNoAdjustment payload for create/update (entity=adjustment). Amounts in the household's currency; category/group/funding account referenced by id; periods YYYY|YYYY-MM|YYYY-MM-DD.
amount_maxNoRule condition: maximum amount (household's currency)
amount_minNoRule condition: minimum amount (household's currency)
member_idsNoParty IDs to merge into merge_into_id (entity=party, action=merge_cluster) — batch form of merge, for a diagnostic-clusterer cluster confirmed as one merchant.
party_typeNoParty type
period_endNoDocument: last day the document covers (YYYY-MM-DD).
provenanceNoDocument: where the filer got it — a Gmail thread id, a Google Drive file id, a portal URL. Free text, recorded as given and never interpreted; searchable as metadata. Inherited by every entry of a batch, like the other document fields.
account_mapNoImport (Monarch): map a Monarch account string (exactly as exported, e.g. "Savings" or "X1 Credit Card (...5316)") to an existing account id. The only way to land a maskless Monarch account on a specific real account (dead-feed backfills).
csv_contentNoRaw statement text — an OFX/QFX/QBO file, or an Apple Card / Rocket Money / Monarch / Wells Fargo CSV; the format is auto-detected from the content. `file_content` is the same field under a name that doesn't say CSV. Small files only; for anything larger use request_upload + upload_id so the bytes don't go through context.
descriptionNoFeedback description — the details behind the title, max 2000 characters.
link_statusNoFilter transfer-link list by status (default: all).
match_traceNoDocument tie_out ONLY: inspect bounded match references for independently imported rows and Plaid separately. No amounts/memos returned or stored. limit caps total trace rows (default 50, max 500); total/truncated report omitted rows. References reflect this observation, not a frozen replay.
position_idNoPosition id: filters position_event list; the default position for a create batch.
text_statusNoDocument list/export ONLY: filter text-index state. queued means awaiting indexing, including reindexing with old searchable pages. Rows show filed_age_days (whole elapsed days since filing, NOT time in the queue), omitted when filing time is unknown.
display_nameNoDisplay name (for account rename)
file_contentNoAlias for csv_content, for non-CSV imports (OFX/QFX/QBO). Pass exactly one of file_content, csv_content or upload_id.
pending_onlyNoPosition_event list: only open call notices (status pending).
period_startNoDocument: first day the document covers (YYYY-MM-DD). A single-dated document (a receipt) sets period_start and period_end to the same day. For a statement PDF, PREFER OMITTING both: the billing cycle is read off the document itself (period_source:"extracted" in the response). A typed (or already-stored) period always wins, and if it disagrees with the document's own cycle the response says so via period_extracted/period_note.
search_scopeNoDocument list/export: where `search` looks — "metadata" (what the filer typed), "text" (the documents' own text only), or "all" (default).
set_party_idNoRule action: set party by ID
set_tag_nameNoRule action: set tag by name
trace_offsetNoDocument tie_out with match_trace:true: skip this many trace rows (default 0). Follow nextOffset; compare source/input and ledger fingerprints between calls because results are live, not a snapshot.
confirm_broadNoRequired to create/update a rule with NO description_pattern/party_pattern (it would match broadly by direction/amount/account alone, on every future sync).
include_emptyNoParty list: also include parties that aren't the effective (displayed) party of any transaction — orphans and rule-overridden auto parties — for finding a rename-collision target or auditing fragmentation. Requires `search` to stay bounded.
inflow_txn_idNoInflow leg transaction ID (for action=link).
merge_into_idNoTarget ID to merge into
party_patternNoRule condition: case-insensitive substring vs the DERIVED party name. Fragile: Plaid can rename an entity-backed merchant on resync, silently breaking the rule (#1211). For consolidation/renaming prefer description_pattern (raw-anchored).
preview_limitNoMax results per page (preview only)
supersedes_idNoDocument create: the id of the document this one replaces (a corrected 1099, a re-issued statement). The old document is marked superseded and its links move to the new one.
with_downloadNoDocument/import get ONLY: opt in to a download object for one stored original (GET URL, headers, expiry, safe filename/content type, exact byte_count and sha256). A host downloads and verifies it to a caller-chosen local path; never put bytes/base64 into context. Import id is the native source id (the upload_id from staging). URLs expire after five minutes; request again for the same id after expiry.
content_lengthNorequest_upload: optional byte size hint.
include_hiddenNoInclude hidden accounts in account lists and their transactions in category/party usage n (excluded by default). Party membership and lifetime eff/auto are unchanged.
outflow_txn_idNoOutflow leg transaction ID (for action=link).
preview_cursorNoPagination cursor (preview only)
recorded_sinceNoPosition_event list ONLY: inclusive recorded-time cutoff (ISO timestamp with timezone, at most milliseconds), regardless of effective date. Pages sort by recorded time, newest first. Keep filters when following cursor; finish all pages before saving terminal as_of for the next poll. Deduplicate ids replayed at the inclusive boundary.
set_is_flaggedNoRule action: mark as flagged
set_is_ignoredNoRule action: mark as ignored
set_party_nameNoRule action: set party by name
allow_connectedNoImport: allow importing into accounts that are also connected via Plaid, to backfill history. OFF by default. When on, rows overlapping the bank's synced window are de-duplicated while CSV-only history is retained. Applies to Apple Card overrides, Wells Fargo targets, and Rocket Money connected accounts.
include_matchesNoRule list: also return matches (how many transactions each listed rule's conditions match now — the same number rule preview's total gives) and last_match (the newest one's date). Counted fresh over every transaction, so pair it with search or a small limit.
remove_tag_nameNoRule action: remove tag by name from matches (system/auto+rule tags only; not a user's manual tag). Mutually exclusive with set_tag_name.
set_category_idNoRule action: set category by global slug (e.g. "food/coffee").
set_is_transferNoRule action: mark/unmark as transfer (true sets, false clears; when applied, overrides automatic transfer classification on future syncs).
statement_checkNoEvent create (batch): the capital account statement's six printed figures, in the position's currency — { beginning, contributions, distributions, fees, change_in_unrealized, ending }. The write is refused unless beginning + contributions − distributions − fees ± change_in_unrealized = ending EXACTLY; all six are required, none is derived.
transaction_idsNoRecurring series create: 1–200 selected posted, nonignored transactions in one direction/currency, not already assigned. Name and frequency required. Use annotate set_recurring with recurring_series_id to assign to an existing series.
include_resolvedNoPosition list: include resolved (exited / written-off) positions, excluded by default.
link_account_idsNoDocument: account ids this document belongs to (a statement's account). Explicit linkage — nothing is inferred.
set_metadata_keyNoRule action: metadata key to set on matches (advanced; set_tag_name covers the common case). Requires set_metadata_value.
transfer_link_idNoTransfer link ID (for confirm/reject; from action=list).
confirm_duplicateNoRule: create/update a rule even though one with identical conditions already exists.
link_position_idsNoDocument: Vested Interest position ids this document is evidence for — a K-1, or a consolidated K-1 package naming every position it covers, which is what marks those K-1s received in query { tax_documents }. Also filters list/export. Explicit linkage — nothing is inferred.
set_category_nameNoRule action: set category by exact name (not group name). List categories first via admin { entity: 'category', action: 'list' }.
include_supersededNoDocument list: include documents that a newer version replaced (excluded by default).
set_metadata_valueNoRule action: metadata value to set on matches. Requires set_metadata_key.
description_patternNoRule condition: substring (or regex) matched against the cleaned description, then the raw bank memo as a fallback — raw-anchored, so it survives Plaid merchant/party churn. Preferred over party_pattern for consolidation/renaming.
link_transaction_idsNoDocument: transaction ids this document is evidence for. Explicit linkage — nothing is inferred.
annotateOrganize transactionsA
Idempotent
Inspect

Categorize, tag, rename, and organize transactions. Set party, category, transfer status, or ignored flag. Bulk operations: apply rules, detect recurring bills and subscriptions. All changes are reversible — raw bank data is never modified.

When reporting back to the user, translate counts and outcomes to plain English ("I categorized 14 transactions"). Don't echo schema field names.

ACTIONS:

  • Classify: categorize, set_party, tag, untag, annotate (set notes/url). Tags are normalized (lowercased, spaces→hyphens — "Greece Trip" stores as "greece-trip"); tag/untag input is normalized to match. untag is source-aware: removing a system (auto) tag also stops it from coming back on the next automatic refresh (re-tagging lifts the suppression); rule-applied tags are refused — edit or delete the rule instead.

  • Reserved tags: 'one-time' excludes a txn from recurring detection/forecast (effective next detection run, or immediately via detect_recurring); 'reimbursable'/'reimbursed' drive query { reimbursables: true }. 'refund' is the system's own marker for a returned charge.

  • Metadata: set_metadata (key+value; one value per key — set replaces), remove_metadata (key, optional value). Custom keys only: "tag"/"notes"/"receipt" are reserved and rejected with a pointer to their dedicated actions. Keys are trimmed + lowercased.

  • Flag: mark_transfer, mark_ignored, set_recurring

  • Revert: revert (clear manual overrides, restore import defaults)

  • Bulk: detect_recurring (creates/updates recurring_series — use preview:true to preview), apply_rules (applies saved rules — use preview:true to preview, rule_ids to limit which rules), infer_categories (fills a placeholder category from the party's own history elsewhere in the ledger; skips manual overrides and any party whose history isn't decisive; preview:true first). detect_recurring (always) and apply_rules with no filter/transaction_ids sweep the whole household: preview:true is a true dry run (no writes, no swept_at) and returns NO token — apply by re-running the same call without preview. NOTE filter.is_uncategorized will NOT find its targets — that means category IS NULL, and these rows carry a placeholder category. Select them with filter.category: ["Other Expense", "Other Income", "Uncategorized"] plus a date range COMPOUND: categorize accepts party_name/party_id to set both in one call. set_party accepts category_name/category_id/group_name to set both in one call.

TARGET: transaction_ids (max 5000) OR filter — one is required for most actions; if both are sent, the filter is ignored and a hint says so. detect_recurring takes no filter (use detect_start). FILTERS: filter.party, filter.category, filter.group, filter.tag, filter.exclude_tag are exact match (case-insensitive), accept string or string[]. filter.country ("GR" / "GRC" / "Greece") and filter.city select by where the merchant is. filter.account matches name or display name (exact, case-insensitive). filter.search is fuzzy text search across display name, party name, description, and check number. Date scope: filter.period (same named periods as query: this_month, last_month, ...) OR filter.start/end (YYYY-MM-DD) — not both. No date scope = ALL TIME. Also: amount_min/max, direction, is_uncategorized, is_recurring, is_transfer, is_ignored, is_pending, is_flagged, has_url, include_hidden. Unknown filter keys are rejected with a validation error (never silently dropped). Date keys at the TOP level (query's flat shape) are also rejected — they belong inside filter. TOKEN (filter-targeted writes only — explicit transaction_ids and filterless sweeps need none): call with preview:true → { matched, token, preview:[sample], will_set? }, review the matched rows and validated category/party/series targets (creates_party says whether a requested party name needs creation), then re-send the SAME action + filter WITH that token to execute (the token param says what makes one stale). Over 20000 matches is rejected: narrow filter.period/filter.account or pass explicit transaction_ids. Preview rows match detail: amt is signed (inflows positive, outflows negative), desc is the displayed name, raw is the original bank memo. RESPONSE: { affected: N, action, matched?, token?, will_set?: { category_id?, party_id?, party_name?, creates_party?, recurring_series_id? }, range?: { start, end } | "all", detail?: { ... }, warnings?: [...], hint? }. range echoes a filter write's effective date scope — check it matches your intent before and after bulk actions. REQUIRED PER ACTION: categorize needs category_id or category_name; set_party needs party_id or party_name; tag/untag need tag_name; annotate needs notes or url (notes:"" clears the note); set_recurring needs recurring_series_id; set_metadata needs key + value; remove_metadata needs key. mark_transfer/mark_ignored/revert need no payload. A missing one is rejected up front, before any write or preview. When affected=0, an error field explains why (e.g. party not found, category not found, missing required field, no matching transactions, missing/stale token) — or, when the requested state already holds (re-applying a tag), a hint says so. When overriding a rule-classified transaction (≤5 explicit IDs, categorize/set_party/mark_transfer/mark_ignored), warnings include rule details and recovery instructions. The write always succeeds; warnings are advisory. NOTE: To explain/debug a transaction's classification, use query { explain: true, transaction_id: "" } instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
endNoDo not use: end belongs inside filter: { filter: { end: ... } }
keyNoMetadata key (for set_metadata/remove_metadata)
urlNoReceipt URL to attach
notesNoNotes to add
startNoDo not use: start belongs inside filter: { filter: { start: ... } }
tokenNoREQUIRED to execute a filter-based write. Get it from a prior preview:true call (the response includes `token`) and pass it back unchanged. It binds the action + the exact set of matched transactions, so the write is rejected if that set, the scope, or the household changed since the preview (including a same-count swap). Not needed when targeting explicit transaction_ids, nor for detect_recurring or a filterless apply_rules (their previews return no token).
valueNoMetadata value (for set_metadata/remove_metadata)
actionYesAction to perform
filterNoTarget transactions by filter
periodNoDo not use: period belongs inside filter: { filter: { period: ... } }
dry_runNoDo not use: use preview: true (there is no dry_run param)
previewNoPreview without writing: matched transactions and validated will_set targets for filter-based targeting (creates_party reports whether a requested party name needs creation), or for detect_recurring the series it would create/update/deactivate and the links it would add/clear. Only a filter-targeted preview returns a token
group_idNoCategory group ID
party_idNoParty to assign
rule_idsNoLimit which rules to apply
tag_nameNoTag name
group_nameNoCategory group name
is_ignoredNoMark as ignored (excluded from all reports)
party_nameNoParty name (creates if needed)
party_typeNoParty type classification
category_idNoCategory to assign
is_transferNoMark as transfer
detect_startNoStart date for detect_recurring lookback (default: 18 months ago). A narrower window re-estimates every matched series from less history and UNLINKS its auto-linked charges older than the window — preview:true first
category_nameNoCategory name — must be an existing category (the taxonomy is a fixed set; use admin category list)
transaction_idsNoTarget specific transactions by ID
overwrite_manualNoOverwrite manual enrichments when applying rules
recurring_series_idNoLink to recurring series

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare it non-read-only, non-destructive, idempotent, but the description adds far more: all changes are reversible and raw bank data is never modified, tag normalization and source-aware untag suppression, reserved metadata keys, stale-token rejection (including same-count swaps), the 20000-match ceiling, warnings versus hard errors on rule overrides, and affected=0 error semantics. These are behavioral traits the structured fields cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long, but for a 14-action, 27-parameter tool the labeled sections (ACTIONS, TARGET, FILTERS, TOKEN, RESPONSE, REQUIRED PER ACTION) keep it navigable and front-load the write semantics. Some segments duplicate the schema's own token/preview text verbatim, which is the main deduction; otherwise nearly every sentence carries actionable information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the full burden and delivers: the response envelope (affected, matched, token, will_set, range, detail, warnings, hint), per-action required payloads, token lifecycle, and error/hint behavior. It also flags the range field as a post-write sanity check, covering the complexity of a bulk multi-action tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the schema is already well annotated, so the baseline is 3; the description earns above that by adding cross-parameter semantics the schema does not: the top-level date-key rejection rule, unknown filter key rejection, one-value-per-metadata-key replacement behavior, the compound categorize/set_party dual-target behavior, and the preview-contract around will_set/creates_party. It clarifies how parameters interact rather than restating individual field docs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening states specific verbs and resources ('Categorize, tag, rename, and organize transactions', 'Set party, category, transfer status, or ignored flag') and enumerates the action set, so an agent knows exactly what class of write operation this is. It also routes the agent elsewhere when appropriate ('To explain/debug a transaction's classification, use query { explain: true }') and points to admin for the category taxonomy, distinguishing it from the read-oriented siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when/when-not guidance is pervasive: preview:true first for detect_recurring/apply_rules/infer_categories, token required for filter-targeted writes but not for explicit transaction_ids or filterless sweeps, transaction_ids OR filter with filter ignored on conflict, and detect_recurring takes no filter. It even gives a concrete workaround when filter.is_uncategorized won't find its targets, which is exactly the kind of alternative-routing guidance this dimension rewards.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

paycheck_planPaycheck planA
Read-onlyIdempotent
Inspect

Show the person's next few paychecks as an interactive card: for each check, the bills and transfers due before the next one lands, a share set aside toward bills that come less often, and what's left; plus whether what's due before the next check is covered by checking + savings today. Built from their detected paycheck and recurring bills — nothing to set up. Use it for budgeting by paycheck: what a check has to cover, how much to put aside each check, what's left after bills. The result is also JSON you can read (amounts positive, in the household's currency, named by ccy). If no regular paycheck is detected it says so — then use query { recurring: true } and query { forecast: true }.

ParametersJSON Schema
NameRequiredDescriptionDefault
checksNoHow many upcoming paychecks to plan (default 3).
spreadNo"lumpy" (default): only bills that come less often than monthly are set aside a share from every check. "all": any bill that comes less often than the paycheck is — rent over weekly pay — is spread across checks too.

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnlyHint/openWorldHint/idempotentHint, but the description adds real behavioral context: it is derived ('Built from their detected paycheck and recurring bills — nothing to set up'), it declares its own failure mode when no paycheck exists, and it specifies return formatting (JSON, positive amounts, household currency, named by ccy). Auth/rate-limit details are absent, keeping this short of a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and output contents are front-loaded in the first sentence, and every subsequent sentence carries unique value (use cases, return format, failure fallback). The opening sentence is dense to the point of being a run-on with several comma-separated clauses, which costs it a point but not readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema and no required parameters, the description compensates fully: it explains the return format and currency semantics, states what happens when the tool cannot produce a plan, and names the fallback tools. Nothing an agent needs in order to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and both parameters carry their own descriptions including defaults and enum meanings, so the schema does the heavy lifting. The description echoes the concept ('next few paychecks', 'a share set aside toward bills that come less often') but adds no syntax, format, or default detail beyond what the schema already states.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific output artifact (an interactive paycheck card) and enumerates exactly what it contains: bills/transfers due before the next check, the share set aside for less-frequent bills, and what's left. It is impossible to confuse with query or accounts, and it even states the precondition (a detected paycheck).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit use cases ('budgeting by paycheck: what a check has to cover, how much to put aside each check, what's left after bills') and routes the agent elsewhere when it cannot apply: 'If no regular paycheck is detected it says so — then use query { recurring: true } and query { forecast: true }.' This is textbook when/when-not/alternative guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

queryLook up finances
Read-onlyIdempotent
Inspect

Query the household ledger — spending, transactions, recurring bills, forecasts, runway, money moved between accounts, plan-vs-actuals, tax prep, and data health. One tool, 20 modes, auto-detected from params.

MODE MAP (first match wins, in this order): digest:true→daily flag briefing · health:true→data-quality dashboard · explain:true+transaction_id→why this name/category · plan_status:true→annual plan vs actuals · vested_interest:true→private holdings register · recurring:true→bills & subscriptions · forecast:true|forecast_months|scenario→projected balance runway · paychecks:true→income streams · account_summary:true→per-account rollup · flows:true→money between own accounts · reimbursables:true→money owed back · tag_report:true→per-tag project ledger · tax_ledger:true→taxes paid/refunded · tax_documents:true→expected tax forms · clusters:true→merchant dedup candidates · detail:true→individual transactions · by+granularity→P&L matrix · granularity alone→trend over time · by alone→aggregate with pct · period alone→income/expenses/net summary

COST: two shapes run an order of magnitude (10-25x) slower than everything else on real households — period:"all", worst combined with search (a substring match defeats the date index and scans lifetime history), and clusters (nothing bounds it, even at its default window). Bound a search with a period where you can. WITHIN a mode the reads already run in parallel, so one mode call is the optimized form — never split a mode into several calls (clusters is the exception: a single serial scan). ACROSS calls, independent queries — two periods, or several modes for a briefing — cost the SLOWEST when issued together in one batch but the SUM when chained, so batch them.

SHAPE: filters go top-level, never nested under a filter key (that is annotate's shape). type:"pnl" nets income−expenses (operating); default type:"cashflow" rows exclude transfers, and its net is the change in cash in every mode (see TYPES).

When reporting results to the user, translate to plain English. Don't echo schema field names like monthly_net, pct, vs_prior, param_warnings — say "money in", "share of total", "change from last month", etc. FIELD GLOSSARY (terse response fields → plain English): amt=amount, cat=category, grp=group, acct=account, bal=balance, avail=available balance, dt=date, n=count, pct=percent, dir=direction (money in/out), freq=how often, xfer=transfer, ign=ignored, pend=pending, ptype=party type, ctype=category type, pfc=the bank's original category (from Plaid), docs=how many documents are filed against this transaction, sign=which way the amounts point ("inflow+ outflow-" = money in positive; "spend+ refund-" = spending positive, refunds negative; "inflow+ outflow-; expenses spend+ refund-" = every section money in positive except the expenses section, which is spending positive).

ADJUSTMENT TRIP TAGS: admin adjustment create/update accepts tags:["trip-slug"] (omit preserves, [] clears). query {tag:"trip-slug",view:"adjusted"} includes only matching applied entries; category/group/exclude_tag compose per leg. query {by:["tag"],type:"expense",view:"adjusted"} shows booked spending plus tagged adjustments, with as_booked_amt and adjustment_amt on contributing rows; n still counts bank transactions. Each tag is independent: never sum overlapping tag rows. Default/as_booked totals stay unchanged. Use summary with reconcile:true for a whole-selection bridge, including an imputation's investment funding offset. A YYYY entry uses period overlap, never a fabricated payment date; finer windows are not proof of when that payment occurred. Tag-report mode remains bank-only and warns if adjusted is requested. Tag child splits/nests and tag matrices retain reporting-entry residuals.

REPORTING VIEWS: view:"as_booked" reports recorded activity and is the basis for cash/statement comparisons with the same account, date and financial-kind scope. view:"adjusted" applies reporting entries without changing those transactions and can differ from cash that moved in the range; reconcile:true shows the bridge. Financial breakdowns, current/prior comparisons, elapsed pacing, FI and expense/savings target formulas follow the displayed financial view. Bank balances, planned recurring facts and historical income shape retain their recorded basis. Reconcile-only keeps displayed amounts and comparisons as booked. A bridge includes scoped reporting-entry kinds and their funding offsets alongside the selected ledger kinds, so an expense-only bridge can carry an investment funding component. Period sums use exact selected dates, even when calendar column labels are wider. P&L operating net remains income minus expenses; change_in_cash includes investment, financing and transfer activity.

REPORTING ENTRY BREAKDOWNS: Reporting entries can lack a real party, account, tag or their own child category. A grouped row then carries reporting_entries.split and/or reporting_entries.items, each {amt,reason}, for that specific alternative child breakdown. Each remainder is already included in the parent; add it only to its own child projection when checking a complete single-valued breakdown. Matrix sections or expense groups carry reporting_entries:{amounts,reason}. Residuals preserve signed reversals and create zero transaction rows. Ordinary top limits can omit primary rows; overlapping transaction tags remain independent and cannot be summed into a partition.

MODE DETAIL (priority order is the MODE MAP above):

  • Digest: { digest: true } → daily "what should I flag" briefing for a scheduled/cron agent: a 30-day low-balance forecast + the attention items (unusual charges, price changes, bank fees, missed bills, late income, capital calls past due or due within 14 days, stale vested-interest marks, vests in the next 30 days and option/warrant expiries within 12 months, broken bank connections), in one bounded read-only call. Optional period/start/end widen the anomaly window (default this month).

  • Health: { health: true } → data quality dashboard: unnamed accounts, uncategorized transaction stats including pending rows (pending_n counts them, including 0 when all are posted), suspected misclassified income (large inflows not in income category), orphan parties, empty categories, stale recurring series (bills that stopped charging), stale party rules (a rule that stopped matching a merchant that keeps charging, or that matches no transactions), orphaned edits (a person's edit stranded where it no longer shows: on a row hidden behind its duplicate or posted copy, a rule left with no condition, a document link or plan reference to something deleted — each item says how to fix it)

  • Explain: { explain: true, transaction_id: "" } → shows how a transaction's name and category were decided. Returns: name-cleanup steps, party match method, rule overrides, the bank's original category (Plaid PFC), all matching rules with scores. Use to debug misclassifications or understand rule behavior before editing. Supports search: { explain: true, search: "STARBUCKS", account: "Wells Fargo" }. Search selects the most recent match and returns matched (full count); use transaction_id for an exact selection. When a plan exists for the transaction's year, includes plan: { year, claimed_by, via? } — which plan item claimed this amount and why.

  • Plan status: { plan_status: true, year?: 2026 } → annual plan vs actuals: income items received/pending/past_due, events with claimed spend + computed status (upcoming/underway/done), everyday-base pacing (whether everyday spending is on track for the year) vs the household's own seasonal shape, guardrails (spending limits), exceptions, full-year forecast. Resolve exceptions by revising the plan (admin { entity: "plan", action: "upsert", reason }) — the plan changes so the breach evaporates on recomputation. No plan for the year → { error, hint } naming admin { entity: "plan", action: "propose", year } to draft one from the household's own actuals. Ignores period/filters — scoped by plan year.

  • Vested Interest: { vested_interest: true } → the register of private holdings (angel checks, fund commitments, advisor equity): per position paid-in, unfunded, distributed, DPI and net cash, open and past-due call notices, vesting on grants, and the latest mark with its as-of date. Add mark:"none" to enumerate positions without a settled mark, or mark:"stale" for open positions whose latest mark is older than 180 days — the gaps behind include:["marks"] coverage, with the usual filters and paging. To read a whole book in one call use rollup_by (groups, not rows) rather than paging; resolved positions come back only as the resolved_omitted count unless include_resolved. Paging, filters, rollup_by, compact, changed_since and include:["marks"] are described on their params. Write side: admin { entity: "position" | "position_event" }.

  • Recurring: { recurring: true } → recurring bills and subscriptions, forecast, duplicates

  • Forecast: { forecast: true } → chronological projected events from recurring series with running balance (within a day, money in first). Projects only series on the balance-pool accounts (checking+savings, or accounts tagged bills when narrowed); series elsewhere (e.g. credit-card subscriptions) are excluded from events and named in warnings. For the full bills list regardless of account, use recurring mode. Open capital-call notices from Vested Interest (admin { entity: "position_event", status: "pending" }) project as dated outflows too — a past-due notice lands on today; a notice projects against the position's funding_account_id; with none set, a pool narrowed to bills accounts cannot place it and names it in the forecast_scope warning, ahead of any recurring series.

  • Paychecks: { paychecks: true } → income-side recurring series with cadence + monthly_avg, plus trailing-90d non-recurring income (variable comp). Filters out inflow series misclassified as transfer/investment/financing via majority-vote categoryType.

  • Account summary: { account_summary: true } → one row per active account with inflow/outflow/net/bal/n. Defaults to last_90d. Use for envelope / multi-account skills. Pass include_hidden:true to include hidden accounts.

  • Flows: { flows: true } → money moved between the user's own accounts (from/to/net/gross). Includes rows where isTransfer=true plus investment and financing category types. Rule-demoted transfers (isTransfer=false on a transfer-typed category) are excluded. Resolves counterpart accounts by name matching. Defaults to last_90d.

  • Reimbursables: { reimbursables: true } → money the user is owed back — outflows tagged 'reimbursable' and not yet 'reimbursed'. Returns { outstanding, n, items, reimbursed_total? }. Tag the expense 'reimbursable' when you pay; add 'reimbursed' once it comes back. Optional period scopes by transaction date (default all time).

  • Tag report: { tag_report: true } → per-tag project/tax ledger: { sign, tags: [{ tag, total, n, first, last, top_categories }] }. Totals a trip/project across categories. A transaction can carry several tags or none, so tag totals overlap and exclude untagged transactions. Read each tag independently; never sum across tags or expect their totals to reconcile to an overall total. Honors ledger filters; defaults to all time. Pages by absolute total descending, then tag: limit 1–200 (default 25), cursor, has_more. Finish when has_more is false for the complete inventory of tags in the selected payment-date window; zero-net tags remain. coverage:{basis:"payment_date",range:{start,end}|"all",tagged_only:true} states coverage explicitly. This is payment-date coverage, not travel dates: prepaid trip charges outside the window are excluded. Keep the same range/type/filters/household/viewer while paging; limit may change. Totals are live, so restart after concurrent edits. Tags are normalized (lowercase-hyphen) — pass "Greece Trip" or "greece-trip", both resolve. total follows the response's sign: "inflow+ outflow-" by default (like detail amt); type:"expense" gives "spend+ refund-" (the sign plan_status uses).

  • Tax ledger: { tax_ledger: true } (US households only; elsewhere { not_available, country }) → what moved between you and tax authorities in a period: { payments: [{ cat, amt, n }], paid, refunded?, refunded_n?, net_paid, covers, excludes }. Payments are Federal/State/Local/Property Tax by category; refunds are inflows from tax authorities. net_paid = paid − refunded. excludes names what's out of view (paycheck withholding isn't visible in the bank feed) — surface it so a W2 user isn't told they paid $0. Defaults to this_year. Organizes what you paid to hand to a preparer — never computes tax owed or gives tax advice.

  • Tax documents: { tax_documents: true } (US households only; elsewhere { not_available, country }) → which tax forms to expect, from the money we can see: { year, basis, expected: [{ form, issuer, issuer_hint?, from, amount, confidence, issuer_confidence, threshold?, filed? }], also_filed?: [{ id, title }], cannot_see: [...], needs_review?: [{ dt?, desc, amt, n?, note? }] }. Wages→W-2 (by employer), interest→1099-INT & dividends→1099-DIV (by account, ≥$10), tuition→1098-T, childcare→statement, charitable ≥$250→acknowledgment. confidence is the FORM ("do you get it?"); issuer_confidence is separate ("do we know who issues it?" — low for payroll-masked employers). cannot_see lists what the cash can't reveal (mortgage 1098, brokerage 1099-B, 1099-R, income we can't see) — surface it so the list is never read as exhaustive. needs_review is possible-1099 income to chase, not a clean prediction. filed is Paper Trail documents for that tax year whose title or filename NAMES that form — what the document SAYS it is, never a classifier; absent means we did not find one, NOT that it hasn't arrived. When several rows share a form (interest is per-account, wages per-employer) the issuer must match too, so one filed 1099-INT can't mark three accounts received; a document naming a form that fits no single row goes to also_filed, alongside filed tax forms nothing predicted. Forms named by description rather than number (childcare statement, charitable acknowledgment) never carry filed. k1? (when the household keeps Vested Interest positions): { expected, received, missing, not_due?, rows: [{ from, via: "vehicle"|"issuer", holder, position_ids, status: "missing"|"not_due"|"received", filed?: [{ id, title, how: "linked"|"name"|"text", page? }] }], unattributed?: [{ id, title, could_be }], text_unread?: [{ id, title, text_status }], not_applicable?: [{ position, instrument, why }], undated?: [{ position, id }], exit_undated?: [{ position, id, last_event }], record_exit? } — one K-1 per vehicle (or issuer, for a direct interest) per holder, for every partnership interest HELD during the year by its own event dates (resolved positions included through their last event): fund/LP instruments, the fund and syndicate segments, anything held through a vehicle, a fund issuer, or an issuer named LLC/LP. A direct SAFE, note, option or warrant is not applicable; an IRA's K-1 goes to its custodian; a position held in a currency other than the household's is assumed to be a foreign fund that issues it no K-1 (each says why in not_applicable). "missing" = the year is over and no K-1 is on file; "not_due" = the year is still running. A K-1 is received when a document for the tax year is linked to the position (admin document link_position_ids — the way a consolidated package names the funds it covers) and names a K-1 or is a tax_form naming no other form, or when its title names a K-1 and its issuer/title fits exactly ONE row (several → unattributed, listing only the rows still open, never attached to all), or — for rows still open — when a page of a K-1 document's indexed text is a partnership K-1 page (Schedule K-1 with Part I "Information About the Partnership", not an estimate or draft) naming that row's vehicle/issuer as a whole-word run — or, for an "X, a series of Y" name a narrow box B wraps, X up to "a series of" with Y's words elsewhere on the page — and no other partnership (how: "text", page = that page). Text never matches a name with fewer than two significant words, a vehicle held by two holders, or a name inside a longer one the book holds, and a series core two book names share finds neither; cover letters, delayed lists and S-corp/trust/state K-1s are not read — link those. text_unread lists K-1 documents the text pass could not read in full (queued, partial past the 200-page index cap, or no text layer) while a row could still be marked by text: a missing row may be inside one. K-1s the checklist takes leave also_filed. undated = pass-through positions with no dated event, so the year can't be decided — record the commitment or an opening balance with held_since; exit_undated = resolved, but the last event isn't an exit, so a later K-1 can't be ruled out — record_exit gives the call that records it (a distribution, return of capital or write-off on its date). Pass year for a prior year (catching a K-1 that never arrived). A position's declared k1 (admin position update { k1: "none" | "expected" }) overrides every rule above, both ways — "none" for an LLC taxed as a corporation or a foreign fund the rules miss, "expected" for one they wrongly rule out; null clears it back to inferred. Defaults to the just-completed tax year (last_year Jan–Apr, else this_year); pass year/period to scope. A checklist to hand a preparer or reconcile against email — never computes tax or gives advice.

  • Clusters: { clusters: true } → find groups of transactions likely from the same merchant but not unified under one party. Default lookback 18 months; pass cluster_months:0 for an all-time dedup sweep of historical merchants.

  • Summary: { period: "this_month" } → income/expenses/net totals. Use period:"all" for all time.

  • Grouped: { by: ["group"], top: 10 } → aggregated by dimension; any grouping containing transaction tag has overlapping buckets and omits total/pct

  • Time series: { granularity: "monthly", periods: 6 } → period-over-period trend. Granularity options: daily, weekly, monthly, quarterly, yearly. periods caps at 90 (enough for last-90-days daily). Default periods is 6 for all granularities except daily, which defaults to 30. Daily labels include weekday (e.g. "Fri May 22"). Daily elapsed comparison is suppressed (use vs_prior on the latest bucket instead).

  • Multi-dim: { by: ["group"], granularity: "monthly" } → income vs. expenses by group over time (a month-by-month income statement). top caps every section's rows, folding the rest into "Other" (by:tag just drops them); nest.top caps the items under each group

  • Detail: { detail: true } → individual transactions with pagination. For a scheduled "what's new since last time" check, add { since: "<as_of from previous run>" } to get only transactions synced since that moment; the response echoes a fresh as_of to store for next time — only once the whole set came back, so follow cursor until has_more is absent before storing it. Add { count: true } to also get matched — the total number of transactions matching the filter across ALL pages — so you can size the result before paging.

SIGN CONVENTIONS:

  • Detail: amt is signed (inflow +, outflow −)

  • Summary: income is money in (positive), expenses are spending (positive; refunds reduce them, so a refund-heavy period can read negative), net = income − expenses + investments + financing + transfers

  • Grouped / time series / multi-dim / tag report / plan_status: see the response's sign. Grouped and time series follow type ("inflow+ outflow-"; type:"expense" → "spend+ refund-"); type:"pnl" and the multi-dim matrix (which ignores type) read "inflow+ outflow-; expenses spend+ refund-". Financing inflows positive, repayments negative.

  • A reconcile bridge carries its own sign ("inflow+ outflow-; expenses spend+ refund-"), whatever the mode's.

  • pct = percentage share; its denominator is described by the response shape

RESPONSE SHAPES:

  • Summary: { range, income, expenses, net, transfers, investments, financing?, top_cats: [{ cat, grp, amt, n, pct }], uncategorized?: { n, amt }, anomalies?: { large_unusual?, price_changes? }, vs_prior?, sources_uses?, fi?, warnings?: [{ kind, msg, party? }], accounts? }

  • Grouped: { range, sign, results: [{ grp|cat|party|acct|tag, amt, n, pct?, items?, split? }], total?, transfers?, net?, net_vs_prior?, vs_prior? }. For grouping without transaction tag, total covers all selected buckets before top truncation. pct uses the sum of absolute bucket net amounts before top, rounded to one decimal with nonzero shares at least 0.1%; true zero stays zero, and rounded shares need not sum to 100%. Under cashflow, net = total + transfers (the change in cash — the same number as summary's net). For the change in cash from the prior period read net_vs_prior (= summary's vs_prior.net); vs_prior tracks total. by:["category"] includes both cat and grp. Any by containing transaction tag omits total, net, pct and grand vs_prior because tags overlap; per-tag amounts and comparisons remain, and untagged transactions can appear as Untagged. type:"pnl" → { income: [...], expenses: [...] } instead (sign "inflow+ outflow-; expenses spend+ refund-").

  • Time series: { granularity, type, sign, periods: [{ label, amt, n, partial?, transfers?, net?, net_vs_prior?, vs_prior? }], averages?: { amt, net? }, stats? }. amt sums the rows; under cashflow, net = amt + transfers (the change in cash — the same number as summary's net) and averages.net is its mean over the complete periods. For net's change from the prior period read net_vs_prior (present only when a transfers line makes it differ; absent, vs_prior is the same comparison); vs_prior/stats track amt. Unfinished simple time-series buckets carry partial:true; vs_prior and net_vs_prior are omitted when either compared bucket is unfinished. stats covers only the COMPLETE periods: when the newest period is still in progress it is dropped and excludes_partial:true is set, so stats.min/max may not appear in periods[]. type:"pnl" → periods have { label, income, expenses, net, savings_rate, change_in_cash, n, partial? } (sign "inflow+ outflow-; expenses spend+ refund-"). change_in_cash = income − expenses + investments + financing + transfers (net cash change, in the household's currency — summary's net; it and savings_rate count every kind whatever include shows, and include only adds the transfers/investments/financing fields and their rows to n); savings_rate = change_in_cash ÷ sources in PERCENT (the cash bucket — same definition as the summary sources_uses block; bounded to ±100, defined in decumulation).

  • Multi-dim: { sign, periods: [...labels], income: { total: [...], items }, expenses: { total: [...], groups: [{ name, total, items? }] }, transfers: { total: [...], items }, investments: { total: [...], items }, financing?: { total: [...], items }, net: [...], vs_prior_elapsed? }

  • Detail: { n, matched?, page_total, txns: [{ id, dt, amt, desc, raw, acct, cat?, grp?, party?, channel?, loc?, xfer?, ign?, flag?, pend?, tags?, docs?, anomaly? }], cursor?, has_more?, as_of? }. page_total is the signed sum of this page's returned txns (money in positive, money out negative), not an all-pages total. as_of appears with since, and only on the page that completes the set. matched (number of filter matches across all pages) appears only with { count: true }. docs is how many documents are filed against that row — absent means none; get them with admin { entity:"document", action:"list", link_transaction_ids:[id] }. channel is "online"/"in store"/"other". loc has city/region/lat/lng/addr when available.

  • Recurring: { series: [{ id, name, renamed?, party, ptype, freq, amt, stddev?, monthly, varies?, dir, ctype, active?, acct, last, next?, expected?, stale? }] (renamed = name is the one the user gave it — show it instead of party; varies = amount swings too much to promise a per-paycheck share; its absence means no claim, not verified-stable), totals: { monthly_income, monthly_expenses, monthly_transfers?, monthly_investments?, monthly_financing?, monthly_net, annual_net }, monthly_sources_uses?: { sources: [{ label, amt, pct }], uses: [{ label, amt, pct }], total, change_in_cash, ratios: { operating_coverage?, net_investing_rate, savings_rate } }, duplicates?, forecast?, missed?, missed_income? }. missed = late BILLS only: money going out to someone else (expenses, loan payments, transfers that pay a card or loan) — moves between the household's own accounts and investment contributions never count. missed_income = late income of at least $25 (scaled to the household's currency), omitted when none — interest cents and round-ups never appear. Note: monthly_net includes transfers/investments/financing (full cash picture). monthly_sources_uses reframes the recurring totals as a balanced statement (change_in_cash == monthly_net) — a recurring portfolio withdrawal shows as a source ("From investments"), useful in decumulation. Its ratios match summary's sources_uses (same units) minus consumption (operating_coverage = recurring income ÷ bills as a ×; net_investing_rate & savings_rate in %). For "bills vs income" compute monthly_income - monthly_expenses. totals/missed/forecast always cover ACTIVE series regardless of is_active (which shapes the series list only); active, recently seen rows show the next date on or after today, matching admin recurring_series list; expected carries the original date when next rolls forward. Inactive and stale rows keep their last expected date as next; stale:true flags active bills that stopped charging or were never observed. type:income/expense narrows the list AND the math.

  • Forecast: { days, starting_bal, ending_bal, p10_ending_bal?, p90_ending_bal?, p10_shortfall_dt?, lowest_bal, lowest_dt, monthly_net, var_net?, var_in?, var_out?, bills_narrowing_active?, events: [{ dt, name, amt, bal, dir, freq }], warnings? }. ending_bal/lowest_bal/lowest_dt and each event's bal fold in a projected daily variable-spend drift (non-recurring everyday flow on the balance pool, beyond recurring bills). lowest_bal/lowest_dt and the Shortfall warnings judge END-OF-DAY balances: a same-day paycheck that covers a bill is no shortfall. The drift is conservative on both sides: var_out = typical recent monthly spending (recent-6-month average), var_in = reliable monthly income (median of the last 12 months, so one-off bonuses don't inflate it); var_net = var_in − var_out per month (negative = net everyday spend; omitted when flat or under ~30 days of history). p10_ending_bal/p90_ending_bal bracket a likely range for the ending balance given how much the user's everyday in/out swings month to month (roughly an 80% range — about a 1-in-10 chance of ending below p10 or above p90); p10_shortfall_dt is when the balance would dip below zero in that pessimistic (p10) case, omitted if it stays positive or history is too short to estimate the swing. monthly_net is recurring-only and spans all matching series (parity with recurring totals); events cover only the balance pool.

  • Paychecks: { paychecks: [{ name, party, ptype, acct, cadence, per_paycheck, monthly_avg, stddev?, last, next }], totals: { monthly_avg, n_streams }, irregular_income_90d }. cadence is one of weekly/biweekly/semimonthly/monthly/quarterly/semiannual/annual. irregular_income_90d sums non-recurring inflows that classify as income — non-zero suggests variable comp or side income outside the regular paycheck.

  • Account summary: { range, accounts: [{ acct, type, inflow, outflow, net, bal, avail?, limit?, n, stale?, stale_since? }], warnings?: [{ kind, msg }] }. One row per active account. Zero-activity accounts still appear so you see the full picture. stale: true means bal is frozen as of stale_since rather than current: the bank connection behind that account is broken or gone (stale_since = the last successful sync, or for an account no connection feeds any more, its last activity), or it still works but the bank has stopped sending updates (stale_since = when it last did; nothing to fix yet, reconnecting is the thing to try if it lasts) — say so rather than quoting the balance as live. Healthy rows omit both keys.

  • Flows: { range, n, flows: [{ from, to, net, n, from_acct_id?, to_acct_id?, gross_out?, gross_in? }] }

  • Clusters: { n, scanned, clusters: [{ name, variants: [{ name, n }], n, sample }], truncated? }. Default top 25; pass top param to adjust. n is total cluster count; truncated shows how many were omitted.

  • Explain: { txn: { id, dt, amt, dir, raw, name_in?, acct }, name: { display, src, stages?, party?, rule? }, class: { cat, grp, type, src, pfc?, rule? }, rules?: [{ name, score, winner?, actions }], plaid?: { merchant, counterparties?, pfc? }, src }. txn.raw is the bank's original text (what a rule's description_pattern matches) and txn.acct the account name — both as detail prints them. name_in is the source's own shorter description (Plaid's cleaned name), which description_pattern also tests and name cleanup starts from; absent means it is the same as raw.

  • Health: { unnamed_accounts?: [{ id, name, mask }], uncategorized?: { n, amt, pending_n, top_parties: [{ name, n, amt }] }, suspected_misclassified_income?: [{ id, dt, amt, desc, cat? }], orphan_parties?: { n }, stale_recurring?: [{ name, last?, freq }], orphaned_edits?: { n, inert_rules?, hidden_edits?, hidden_metadata?, document_links?, plan_refs?: { n, items: [{ …, fix }] } } }. unnamed_accounts lists unrenamed blank, number-like, known-mask or generic bank-product names; readable bank-provided names need no rename. Unused global categories are not health issues. All fields omitted when clean.

  • Digest: { as_of, range, forecast: { days, ending_bal, lowest_bal, lowest_dt, var_net?, warnings?, shortfall_kind?, assumes? }, attention: { unusual, price_changes, bank_fees, missed_bills, missed_income, broken_connections, plan_exceptions?, capital_calls?: [{ position, id, amt, ccy, due, past_due? }], stale_marks?: [{ position, as_of, age_days, source }], newer_recorded_marks?, vests_soon?: [{ position, dt, qty }], expiries?: [{ position, expiry, days, expired?, exercise_cost? }], uncategorized? } }. A checked section with nothing in it is [] (checked, none); plan_exceptions appears only with a plan for the year, and the Vested Interest lists only with an open position. With a shortfall, forecast.assumes says what the projection counted: { through, start_bal, in: [{ dt, name, amt, freq }], out: [...], more_in?/more_out?: { n, amt }, drift?, late_excl?: { n, amt } } — the largest projected events each side up to the dip (amt signed in+ out-), start_bal + every event's amt + drift = lowest_bal when the conservative line dips (drift is absent when only the P10 band dips, through = p10_shortfall_dt). Late bills are never projected; late_excl counts them.

  • Vested Interest: { n, positions: [{ id, name, issuer, issuer_kind, holder, holder_kind, beneficial_owner?, instrument, segment?, ticker?, k1?, terms?: [{ term, value?, basis, doc?, page?, note? }], ccy, status, funding_account_id?, committed, paid_in, unfunded, distributed, cost_basis, dpi?, net_cash, held_days?, vested?, unvested?, vested_through?, schedule_through?, exercised?, next_vest?: { dt, qty }, exercise_cost?, pending?: [{ id, amt, due, past_due? }], converted?: { dt, from?, to?, qty? }, instrument_stale?, mark?: { amt, as_of, source, basis?, age_days, stale?, recorded?, newer_recorded?: { amt, as_of, recorded, source } } }], totals?: { : { n, committed, paid_in, unfunded, distributed, cost_basis, net_cash, pending_calls } }, past_due?: [{ position, id, amt, ccy, due }], stale_marks?: [{ position, as_of, age_days, source }], newer_recorded_marks?: [{ position, amt, as_of, recorded, source }], vests_soon?: [{ position, dt, qty }], expiries?: [{ position, expiry, days, expired?, exercise_cost? }], caveat }. All amounts in each position's own currency. unfunded = committed − contributions: fees and exercises are paid-in but never draw a commitment. vests_soon = open positions whose NEXT tranche vests within 30 days (a later tranche is still on the row's next_vest); expiries = within 365 days, or past. converted = the latest conversion (qty = shares received); instrument_stale = the position still declares the instrument it converted from — update its instrument. unvested is OMITTED rather than 0 when no future tranche is recorded — absent means unknown, not fully vested; vested_through = last tranche on or before today; schedule_through (only when a future tranche is recorded) = last tranche on record, not a promise the schedule is complete. stale = a mark older than 180 days (never on a resolved position); newer_recorded names a mark recorded LATER whose as-of sits behind this one.

  • Plan status: { plan: { year, rev, created, revised_at? }, sign, income?: [{ name, expected, actual?, due_by?, status?, unmatched? }], events?: [{ name, tag?, est, actual, n?, last?, status, window?, over_pct? }], base: { annual, ytd_actual, ytd_expected, pace_pct?, shape_source, shape_note? }, guardrails?: [{ scope, label?, min?, max?, actual, state, no_match? (nothing matched yet this year — normal early on or off-season; a scope matching nothing at all raises a guardrail_empty exception) }], exceptions?: [{ key, kind, msg }], forecast: { projected, actuals_ytd, events_remaining, base_remaining, by_month } }. All amounts in the household's currency. sign is "spend+ refund-; income in+": event, base and guardrail amounts (and the exceptions quoting them) are spend-positive — outflows add, refunds subtract — so an event's actual reads opposite to its tag's default tag_report total. No plan for the year → { error, hint }.

ZERO-RESULT LOOKUPS: Initial search/amount lookups in detail, grouped and simple time series can return excluded_transfers:{n,hint}, a positive transaction row count of matching transfers outside the primary results. Retry the same filters as the hint directs: default cashflow uses is_transfer:true; P&L and income/expense/financing require type:"all" plus is_transfer:true. Grouped P&L requires that retry even with include:["transfers"]. An explicit exclusion stays honored: replace is_transfer:false with true to inspect those rows. This diagnostic does not change counts, totals or paging and is absent on continuation pages and when no matching omitted transfers are found.

PAGINATION: Pass the returned cursor value as cursor in the next call to page forward. Cursors are bound to the sort that issued them — keep the same sort across pages (changing sort mid-pagination is a validation error; start over without the cursor instead).

TYPES:

  • income: income only (excludes investments + financing)

  • expense: expenses only (excludes investments + financing)

  • financing: cash advances + EWA-repaid-from-bank only (DTC borrowing flows: Cleo, Brigit, Dave, etc.)

  • pnl: income+expenses side-by-side (excludes transfers/investments/financing). Its net is the OPERATING net (income − expenses). Grouped/time-series split into income[] and expenses[] — expenses spend-positive, the rest money-in positive (see sign). Cash-basis income statement — actuals only; for plan-vs-actuals use plan_status mode.

  • cashflow (default): includes investments + financing, excludes transfers from rows and row totals (unless is_transfer:true). Its net is ONE number in summary, grouped, time series and the matrix: the change in cash = income − expenses + investments + financing + the transfers line, which is non-zero when a transfer's other leg isn't in view (an unconnected or hidden account, or across the period boundary). Grouped total / time-series amt are the row sums without that line.

ADJUSTED REPORTS: view:"as_booked" (default) reports original bank activity. view:"adjusted" includes reporting entries managed by admin { entity:"adjustment" }, including imputation for real expenses paid outside connected bank feeds. reconcile:true adds original versus adjusted totals and entry effects to summary/time-series output. Use imputation only when investment-withdrawal funding matches the payment. Adjusted reports support summary, time series, and grouped reports with exactly one group/category/tag dimension and type other than pnl. Category/group filters select each leg separately; tag/exclude_tag select the entry's tags on both its expense and funding legs. Party, account and other unsupported transaction filters disable the overlay: check view and param_warnings for as_booked fallback. Tags overlap: never sum tag rows. tag_report and detail remain bank-only; tag child breakdowns and matrices keep a reporting-entry remainder. Adjustments create no bank transactions. Coarse YYYY or YYYY-MM periods count in full on any overlap, without proration, and land in the first overlapping trend bucket; use an exact YYYY-MM-DD payment date when known. Retire the imputation if the real payment is later imported to avoid counting it twice.

FILTERS: category, group, party, account, tag, exclude_tag, country, city, search, amount_min/max, direction, is_recurring, is_uncategorized, is_transfer, is_ignored, is_pending, is_flagged, has_url, include_hidden. Scope note: "hidden" applies to ACCOUNTS (include_hidden brings hidden accounts back in); "ignored" applies to individual TRANSACTIONS (is_ignored finds them). exclude_tag drops rows carrying any of the named tags (none-of-any); ordinary labels and untagged rows remain, and [] excludes nothing. Transaction tags are normalized (lowercased, spaces→hyphens — "Greece Trip" → "greece-trip"), so filter input is normalized to match. Tagging a txn 'one-time' also excludes it from recurring detection / forecast. FILTER COMBINATIONS: filters AND together. Useful combos: { account, is_recurring: false, period, by: ["category"] } isolates non-recurring drafts on one account (good for reconciling against per-account net flow). { account, period } gives that account's income/expenses/net. { account, tag, period } scopes a tag to one account. SORT (detail mode): sort="amount"|"-amount" (default "-date"). Amounts are signed — for largest expenses use sort="amount" (most negative first); for largest inflows use sort="-amount" (most positive first). Detail limit is 1–200 (default 25); grouped top is 1–50 (default 50), and limit does not cap grouped results. HIDDEN ACCOUNTS: Transactions from hidden accounts are excluded by default. Pass include_hidden:true to include them. Manage visibility via admin { entity: "account", action: "hide"|"unhide", id: "" }. SEARCH DEFAULT: When search is provided with no period/start/end, defaults to all time. COMPARE: prior_period, prior_year — works in summary and grouped modes with a bounded period (not 'all'). The change in net from the prior period is summary's vs_prior.net and grouped's net_vs_prior (the same number). prior_period_elapsed (defined on the compare param) also works in multi-dim time series (not at daily granularity), where it surfaces as vs_prior_elapsed. Check param_warnings. INCLUDE: stats, warnings, accounts, transfers, investments, financing, fi (extra output sections). transfers/investments/financing add those fields to pnl time series periods. fi adds FIRE primitives (annual_spend, fi_number_25x, fi_lean_20x, fi_chubby_30x, fi_fat_50x, years_to_fi, extrapolation_basis, extrapolation_reliability, decumulation?) to summary mode. targets adds the 80/20 framework defaults: income_shape, buffer { months, target, callable?, current_months, shortfall?|funded } — target = months × expenses + callable, where callable is Vested Interest's unfunded USD commitments not yet noticed (committed − contributions; money owed on demand, so a reserve rather than a forecast event) — and savings_rate { current_pct, default_pct?, gap_pct? }. ANOMALIES (default-on in summary mode): { large_unusual: [{ desc, amt, dt, party?, vs_avg, reason }], price_changes: [{ name, expected, actual, pct_change, reason }], bank_fees: [{ desc, amt, dt, pfc, kind, reason }], by_account?: [{ acct, total, n }] }. Pre-computed at import time. large_unusual flags per-party transactions exceeding historical median (MAD-based, log-space). price_changes flags recurring series whose recent median deviates from expected amount. bank_fees surfaces NSF / overdraft / late fees (from Plaid PFC). by_account rolls up large_unusual by account so a spending skill can quote "Bill Pay had $X of irregular drafts" without re-summing. ANOMALY TAGS: each fast-detector anomaly also writes a transaction_metadata tag for cheap filtering — nsf, overdraft, late-fee, price-increase / price-decrease (sign-aware). Use query { detail: true, tag: "nsf" } to drill into bank fees without filtering on anomalyType directly. SOURCES_USES (summary mode, always-on when there's activity): a Sources & Uses of cash statement that works in BOTH FIRE accumulation and retirement/decumulation — each flow lands on a side by DIRECTION, so investments are a use when contributing, a source when withdrawing. Shape: sources[] and uses[] (each { label, sub?, amt, pct } where pct is % of the equal side total), total, change_in_cash (signed; + added to cash, − drawn down; == net exactly), and ratios: operating_coverage (income÷expenses as a MULTIPLE/×, e.g. 7.4× or 0.24× — not a percent; >1 accumulation, <1 retirement; omitted when no spend), net_investing_rate (−investments÷sources, in PERCENT; its SIGN is the regime flag — + accumulating, − decumulating), savings_rate (change_in_cash÷sources, in PERCENT — the CASH bucket: cash set aside (+) / drawn down (−) after consuming and investing; Sethi's "Savings" bucket, distinct from and non-overlapping with net_investing_rate, so FIRE's total savings rate = savings_rate + net_investing_rate; always present), consumption { essential, discretionary, untagged, reporting_entries? } (each { amt, pct } as % of total sources; reporting_entries also explains why its signed expense amount lacks an attributable category). The three classified buckets plus that reporting-entry remainder equal displayed consumption. The whole block is one sources-denominated frame: consumption + investing + savings partition total cash inflow. LOCATION: country / city are the MERCHANT's location as the bank reports it, so an online merchant abroad counts as abroad. A trip: { detail: true, start, end, country: "GR" } finds every charge made there in the window, whatever its category. GROUPING: Category/group buckets partition the same selected transactions (including Uncategorized) and add up before top truncation, unless combined with transaction tag. Transaction tags overlap: one purchase can appear under several labels, so never add the tag rows together. Grouped tag results can include Untagged; tag_report excludes untagged transactions. Matrix headline totals are computed independently of overlapping tag rows. Canonical category tags are different: essential/discretionary/untagged are mutually exclusive. by:["category_tag"] aggregates by canonical category tag (essential / discretionary / untagged). Works in by:, nest.by:, and split_by:. Most useful with type=expense — non-expense categories carry no canon tag and roll into 'untagged' (a param_warning fires on type=income or type=financing). Response key is c_tag. For "P&L split essential / discretionary": by:["category_tag"], type:"expense". For "each spending group split by spending-class": by:["group"], split_by:"category_tag", type:"expense". For per-account or per-owner rollups, use the existing by:["account"] with an account:[...] filter (account_tag grouping is intentionally not exposed — accounts are low-cardinality enough that the model handles them directly). WARNINGS: { kind, msg, party? } objects. unusual_txn includes the resolved party when known, independently of the transaction display label in msg. Summary mode: always-on sync_incomplete + connection_error + flagged + anomaly warnings (bank_fee, price_increase, missed_bill, missed_income); with include:["warnings"] also low_balance, unusual_txn, uncategorized. missed_bill, missed_income and low_balance describe today, so they appear only when the range includes today — their absence on a past range says nothing about bills or balances. Account-summary mode: the connection warnings only (sync_incomplete, connection_error, connection_warning), matching summary word for word. PRECEDENCE: start/end beats period, and params that belong to a lower-priority mode than the one detected are ignored. Both are reported in param_warnings. NEST: { by: "party", top: 5 } — sub-group within each grouped result SPLIT_BY: any by: dimension — cross-tab each grouped result by a second dimension RECURRING OPTIONS: forecast_days (0-365, recurring default 30, forecast default 90), is_active (default true). Also accepted by paychecks: is_active, include_hidden.

DISCOVERY: To list available categories, groups, parties, or tags, use admin { entity: "category"|"party"|..., action: "list" }. DASHBOARD: https://cashflow.tech has two views agents can link to:

  • /spending?start=YYYY-MM-DD&end=YYYY-MM-DD (income statement). Also accepts view=month|quarter|year|all_time for named periods.

  • /transactions?start=YYYY-MM-DD&end=YYYY-MM-DD&search=X&group=X&account=X (transaction list). Also accepts view param. All filter params optional.

ParametersJSON Schema
NameRequiredDescriptionDefault
byNoAggregate by dimension(s). Triggers grouped mode. category/group put each selected transaction in one bucket, including Uncategorized; their full buckets add up before top truncation. Any grouping containing transaction tag has overlapping buckets: a transaction can carry several tags or none. Read each tag independently; never sum across tags. Grouped tag results omit total/pct and can include Untagged; tag_report gives standalone per-tag figures without Untagged. category_tag is different: its canonical essential / discretionary / untagged buckets do partition the selected transactions; most useful with type=expense. country groups by merchant country (ISO alpha-2), city by merchant city; rows with none land in "Unknown".
endNoCustom range end (YYYY-MM-DD)
tagNoTransaction tag name(s) — exact match
topNoLimit grouped results to top N by amount (1–50, default 50); grouped comparisons rank by absolute change and include rows with only prior activity. In the time-series matrix (by + granularity) it caps every section's rows: expense groups past N fold into "Other" so groups still sum to the total (over by:tag, whose buckets overlap, the extra tags are dropped, not folded); nest.top caps the items under each group.
cityNoMerchant city — exact match, case-insensitive. Only where the bank reports one (mostly linked accounts).
markNovested_interest: only positions with no settled mark (none) or whose latest settled mark is stale (stale: older than 180 days, open positions only). A settled zero-dollar mark is marked; pending and superseded marks do not count. Uses the same status as include:["marks"] coverage and stale_marks. Composes with other book filters; totals, matched, alerts, resolved_omitted and rollups cover this filtered set before paging.
nestNoNested sub-grouping within each group
sortNoSort field (detail mode): date, -date, amount, -amount. Default -date. Signed amounts: amount puts the most negative outflows first; -amount puts the most positive inflows first. A typo is a hard error, not a silent fall back to date.
typeNoincome, expense, financing (cash advances + EWA repaid from bank), pnl (income+expenses side-by-side, excludes transfers/investments/financing; its net is the OPERATING net = income − expenses, while a pnl trend's change_in_cash and savings_rate still count every kind), cashflow (default; rows and row totals exclude transfers, and `net` — one number in summary, grouped, time series and the matrix — is the change in cash = income − expenses + investments + financing + the transfers line, which is non-zero when a transfer's other leg isn't in view), all (full ledger, no kind filter — includes transfers)cashflow
viewNoReporting view (summary, grouped, time-series). 'as_booked' (default; alias 'actual') = recorded activity for cash/statement comparisons with the same account, dates and financial-kind scope. 'adjusted' applies reporting entries without changing those transactions, so it can differ from cash that moved in that range. Period sums use the exact selected dates even when calendar labels are wider. Under `compare`, adjusted compares to adjusted. category/group/tag filters (and their exclude_*) scope the overlay; other narrowing filters return as_booked. Adjusted single by:['tag'] rows include as_booked_amt and adjustment_amt; tags overlap, so never sum rows. A year-only adjustment keeps its year period (overlap, not a payment date). Manage entries via admin { entity: 'adjustment' }.as_booked
yearNoYear for plan_status (plan year) and tax_documents (tax year). Default: current year.
countNoDetail mode only: also return `matched` — the total number of transactions matching the filter across ALL pages — so you can size the result before paging. Opt-in because it runs a second count scan (vested_interest always returns matched).
flowsNoNet inter-account transfer matrix (from/to/net/gross). Includes transfers, investment flows, credit card payments.
groupNoCategory group name(s) — exact match, case-insensitive
limitNoPage size 1–200: detail and tag_report default 25, vested_interest defaults 50. Ignored in grouped mode — use top (1–50, default 50) there.
partyNoParty name(s) — exact match, case-insensitive
sinceNoDetail mode only: transactions recorded on or after this ISO timestamp — the `as_of` from a previous run. The first page pins an upper recorded-time bound carried by its cursors, so later arrivals wait for the next run. Keep the original since and sort while paging; store as_of only after has_more is absent. Timestamp equality may replay boundary rows: deduplicate by transaction id.
startNoCustom range start (YYYY-MM-DD)
cursorNoPagination cursor from previous response. Pass the exact cursor to fetch the next page. tag_report cursors require unchanged payment-date range, type, filters, household and viewer; limit may change. Its live totals can move during concurrent edits: restart for a fresh inventory.
detailNoReturn individual transactions instead of aggregates
digestNoDaily 'what should I flag' digest: a 30-day low-balance forecast, missed bills (late payments out, never moves between your own accounts), late income (paychecks and other deposits of $25 or more that haven't arrived), unusual charges (large/price-change/fees), plan exceptions when a plan exists (breached spending limits, missed income, over-budget events, off-pace spending), and broken bank connections, in one bounded read-only call. Built for a scheduled/cron agent — safe to run every morning.
filterNoDo NOT use — filters go at the top level (e.g. { tag: 'wechat' }). A nested wrapper is flattened, with a param_warnings nudge.
healthNoData quality dashboard: accounts needing readable names, uncategorized transactions, suspected misclassified income, orphan parties, stale recurring
holderNovested_interest: only positions whose legal holder (the person, trust, LLC, IRA or nominee) contains this text, case-insensitive.
issuerNovested_interest: only positions whose issuer (the company or fund) contains this text, case-insensitive.
periodNoNamed time period. 'all' = no date filtering.
searchNoText search across display name, party name, description, and check number
accountNoAccount name or display name — exact match, case-insensitive
compactNovested_interest: omit position notes, term notes and mark basis explanations from the rows. Keeps exact term values and their basis/document/page provenance, plus mark amounts, dates and sources. Opt-in, including with changed_since; use smaller limit pages for large books. Full prose remains available through compact:false or admin position get.
compareNoAdd comparison to prior period or prior year. prior_period_elapsed compares only the same number of elapsed days of the prior period (apples-to-apples for an in-progress period like this_month); prior_year_elapsed does the same against the SAME span one year earlier (right for a partial year — H1-2026 vs H1-2025, not vs the preceding H2-2025). Both work in summary, grouped, and multi-dim time series.
countryNoMerchant country — ISO alpha-2 ("GR"), alpha-3 ("GRC") or English name ("Greece"), any case. The merchant's country as the bank or card network reports it; rows with no known country never match.
explainNoProvenance trace for a single transaction's naming and classification. Search selects the most recent match and returns matched (the full match count); use transaction_id for an exact selection.
has_urlNoFilter by receipt URL presence (true=has, false=missing)
includeNoExtra sections to include in output. transfers/investments/financing: include in pnl time series output. fi: FIRE primitives (annual_spend, fi_number_25x, lean/chubby/fat targets, years_to_fi) on summary. targets: 80/20 framework defaults on summary — emergency-buffer target sized by income shape (with months-short) and a savings-rate anchor vs the household's actual rate. marks: mark COVERAGE on vested_interest — marked_n, unmarked_n, stale_n and the as_of span of the marks that exist, per currency. Counts and dates only: marks are recorded opinions of different ages and are never summed into a total, so it answers how much of the book carries a current opinion, not what the book is worth.
periodsNoNumber of rolling periods (requires granularity). Cap is 90 so 'last 90 days daily' fits; for longer ranges use a coarser granularity.
segmentNovested_interest: only this book — employment, direct, fund, syndicate, other. The employment book in one call. "undeclared" returns the positions that declare NO segment — nothing else reaches those, since the column is nullable with no default and they match no other value.
vehicleNovested_interest: only positions held through a vehicle whose name contains this text, case-insensitive. "undeclared" returns the positions held through NO vehicle (a direct deal, an employment grant).
categoryNoCategory name(s) — exact match, case-insensitive
clustersNoFind groups of transactions that likely represent the same merchant but aren't unified under one party
currencyNovested_interest: only positions in this currency (USD, NZD, …).
end_dateNoAnchor date for the time window — shifts named periods and rolling buckets to end on/around this date instead of today. Applies in every mode.
forecastNoChronological projected events from recurring series with running balance
scenarioNoForecast decision-support: re-run the projection with these what-if changes to regular bills/income, and compare to the unchanged baseline. Answers 'can I afford X?' / 'what if income drops?'. Works with forecast or forecast_months; nothing is saved.
split_byNoCross-tab: add a split map by this dimension to each grouped result
directionNoFilter by transaction direction
is_activeNoFilter active/inactive series (recurring mode, null=all, default true)
paychecksNoIncome-side recurring series only (paychecks, regular deposits) with cadence labels, plus trailing-90d non-recurring income for variable comp
reconcileNoAttach a reconciliation bridge to summary or time-series OUTPUT: recorded as_booked activity vs adjusted reporting totals plus each entry's net effect within the exact selected dates. A modifier, not a mode of its own — it adds to whatever the other params select, and is scoped by period/start/end like any other query (never by `year`). Works with either view; vs_prior follows `view`, not the bridge.
recurringNoList recurring bills/subscriptions from recurring_series table
rollup_byNovested_interest: return GROUPS instead of rows — per group its included position count, resolved_omitted count when resolved positions were left out, and its per-currency totals, and no positions at all. A resolved-only group has n:0 and empty totals; include_resolved counts those positions and omits resolved_omitted. The whole register in one call: 86 positions is ~78KB of rows and past most tool-result limits, where the same book by segment is a handful of lines. Each group names its DIMENSION as the key ({ segment: "syndicate", n, totals }), so the value can be sent straight back as that filter; a position with no value for the dimension buckets under "undeclared". Groups cover the whole filtered set, so limit and cursor do not apply — there are no rows to page.
amount_maxNoMaximum absolute amount (household's currency)
amount_minNoMinimum absolute amount (household's currency)
instrumentNovested_interest: only positions of this instrument (option, safe, private_equity_fund, …).
is_flaggedNoFilter flagged transactions
is_ignoredNoReturn ONLY ignored (hidden) transactions — they're excluded from every query by default. To see ignored rows ALONGSIDE normal ones (e.g. a search that greys them out), use include_ignored instead.
is_pendingNoFilter by pending status — true = only pending, false = only posted. Omitted: transaction detail shows both; summaries and reports show posted only
tag_reportNoPer-tag project/tax ledger: each tag's total, count, date span (first/last), and top categories. A trip/project total across categories. Transactions can carry several tags or none: these totals overlap and exclude untagged transactions. Read each tag on its own; never sum across tags or expect them to reconcile to an overall total. Honors ledger filters; defaults to all time. Pages by absolute total descending, then tag: limit 1–200 (default 25), cursor and has_more. Finish when has_more is false for complete tag inventory in the selected payment-date window. coverage states payment_date and range (or all); it excludes untagged rows and prepaid trip charges outside that window, so this is not travel-date coverage. Per-tag totals include each transaction once. Live pagination: restart after concurrent edits.
tax_ledgerNoWhat you paid the government this year: tax payments by category (Federal/State/Local/Property Tax), refunds from tax authorities, and net_paid. Returns an `excludes` note (e.g. paycheck withholding isn't visible in the bank feed) so a W2 user is never told they paid $0. Defaults to this_year. Organizes what happened to hand to a preparer — not tax advice. US households only.
account_tagNoAccount tag name(s) — restricts to transactions in accounts carrying any of the tags. Canon: operating, bills, emergency-fund, savings-goal, brokerage, retirement.
exclude_tagNoExclude transaction tag name(s) — drops rows carrying any of these normalized tags (none-of-any, exact match). Other labels and untagged rows remain; [] excludes nothing.
granularityNoGroup results by day, week, month, quarter, or year. Triggers time series mode.
is_transferNoFilter transfers
plan_statusNoAnnual plan vs actuals: income items, events (claimed spend + computed upcoming/underway/done), everyday-base pacing vs the household's seasonal shape, guardrails, exceptions, and the full-year forecast. Scoped by plan year (ignores period/filters).
terms_basisNovested_interest: only positions carrying at least one term on any of these bases. One value or a list, so terms_basis:["assumed","missing"] is the whole what-only-you-can-confirm list in ONE call.
category_tagNoCategory tag name(s) — restricts to transactions whose category carries any of the tags. Canon: essential, discretionary.
is_recurringNoFilter recurring transactions
private_bookNoDeprecated: renamed vested_interest. Still accepted, with a param_warning — send vested_interest.
changed_sinceNovested_interest: latest currently matching positions with something RECORDED on or after this (inclusive): an event or position creation/update, with changes. Date and naive ISO mean UTC; offset timestamps keep their exact instant; at most 3 fractional digits and normalized UTC year 0000–9999. Page to completion before saving as_of, the FIRST request clock carried in a scoped versioned cursor. Keep changed_since and book filters unchanged; legacy changed-feed cursors require restarting without cursor from the original cutoff. Next review from as_of intentionally overlaps concurrent records: replace/upsert by position id, never add totals across pages/runs. Membership, folds, totals and alerts stay live; rollups have no feed cursor/as_of. Future row cutoff completes empty at the current clock. Timestamp order is not commit order: earlier-stamped late commits/backward clocks, event edits/deletes, position deletions and filter exits have no guaranteed notifications; position edits coalesce to updated:true without a diff.
exclude_groupNoExclude category group name(s)
exclude_partyNoExclude party name(s)
forecast_daysNoDays to forecast (recurring mode default 30, forecast mode default 90, 0 to skip)
reimbursablesNoOutstanding receivables — outflows tagged 'reimbursable' and not yet 'reimbursed'. Returns the total you're owed plus the open items.
tax_documentsNoWhich tax forms to expect, derived from the money Cashflow can see: wages→W-2, interest→1099-INT, dividends→1099-DIV (kept per issuer), plus categorized tuition/childcare/charitable → 1098-T/statement/acknowledgment. Each form carries its issuer, amount, and confidence (form vs issuer). Returns a `cannot_see` list (mortgage 1098, 1099-B, 1099-R, income we can't see…) so it's never read as exhaustive, and `needs_review` for possible-1099 income to chase. Defaults to the just-completed tax year (last_year Jan–Apr, else this_year); pass `year`/`period` to scope. A checklist to hand a preparer or reconcile against email — not a return, not tax advice. US households only.
cluster_monthsNoClusters mode: lookback window in months (default 18). Pass 0 for all-time — use for a full dedup sweep of historical merchants, since merchants whose last activity predates the window are otherwise invisible.
include_hiddenNoInclude hidden account transactions (excluded by default)
transaction_idNoTarget transaction ID (for explain mode)
account_summaryNoPer-account inflow/outflow/net/balance/n over a period — one row per active account
forecast_monthsNoForecast mode: return a month-by-month projection over N months (1-12) with a calibrated low/high (P10/P90) balance band, instead of the daily event list. Headline is first_negative_month — the first month the low-end (P10) balance goes negative (the runway-relevant stress point).
include_ignoredNoInclude ignored (hidden) transactions ALONGSIDE normal ones instead of excluding them; each ignored row is flagged with `ign:true` so a caller can de-emphasize (grey out) rather than drop it. Unlike is_ignored:true (only ignored), this returns both. Ignored still stays out of all aggregate totals.
vested_interestNoThe register of private holdings (angel checks, fund commitments, advisor equity): per position committed / paid-in / unfunded / distributed / DPI / net cash, open and past-due capital-call notices, and the latest mark with its as-of date and a staleness flag. Pages (limit, default 50, + cursor); resolved positions are left out unless include_resolved, and their count is echoed as resolved_omitted. Filters: instrument, issuer, vehicle, currency, mark, search (name / issuer / vehicle). totals, matched and the alerts cover the WHOLE filtered set, not the page. Totals per currency, never combined; marks never summed. cost_basis (per position and in the totals) is what is still AT RISK — paid_in minus return_of_capital minus written_off — not what went in; return_of_capital / fees / written_off print beside it whenever they are non-zero, so a write-off is never a silent gap between the two. On a resolved position net_cash is the realized result and dpi the realized multiple. Ignores period and the ledger filters.
exclude_categoryNoExclude category name(s)
include_resolvedNovested_interest: also list resolved (exited / written-off) positions — left out by default, as in admin position list.
is_uncategorizedNoFilter uncategorized transactions
recurring_series_idNoOnly transactions linked to this recurring series ID, from query { recurring: true }. Matches membership, not merchant or series name. Use period:'all' for its full history.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updates
    • Changedadmin1 field changed
      • addedInput schema / properties / adjustment / properties / tags
        Added value: +{
        +  "description": "Household transaction tags (for example a trip slug). Replaces the full set; omit to preserve, [] to remove all. Normalized like transaction tags. Applies to both the expense and funding legs; expense reports show the expense only.",
        +  "items": {
        +    "maxLength": 200,
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 50,
        +  "type": "array"
        +}
    • Changedquery1 field changed
      • changedInput schema / properties / view / description
        Previous value: -"Reporting view (summary, grouped, time-series). 'as_booked' (default; alias 'actual') = recorded activity for cash/statement comparisons with the same account, dates and financial-kind scope. 'adjusted' applies reporting entries without changing those transactions, so it can differ from cash that moved in that range. Period sums use the exact selected dates even when calendar labels are wider. Under `compare`, adjusted compares to adjusted. category/group filters (and their exclude_*) scope the overlay; any other filter returns as_booked. Manage entries via admin { entity: 'adjustment' }."New value: +"Reporting view (summary, grouped, time-series). 'as_booked' (default; alias 'actual') = recorded activity for cash/statement comparisons with the same account, dates and financial-kind scope. 'adjusted' applies reporting entries without changing those transactions, so it can differ from cash that moved in that range. Period sums use the exact selected dates even when calendar labels are wider. Under `compare`, adjusted compares to adjusted. category/group/tag filters (and their exclude_*) scope the overlay; other narrowing filters return as_booked. Adjusted single by:['tag'] rows include as_booked_amt and adjustment_amt; tags overlap, so never sum rows. A year-only adjustment keeps its year period (overlap, not a payment date). Manage entries via admin { entity: 'adjustment' }."
  2. 1 tool update
    • Changedquery6 fields changed
      • changedInput schema / properties / cursor / description
        Previous value: -"Pagination cursor from previous response. Pass the cursor value returned in the prior call to fetch the next page."New value: +"Pagination cursor from previous response. Pass the exact cursor to fetch the next page. tag_report cursors require unchanged payment-date range, type, filters, household and viewer; limit may change. Its live totals can move during concurrent edits: restart for a fresh inventory."
      • changedInput schema / properties / exclude_tag / description
        Previous value: -"Exclude transaction tag name(s) — drops rows carrying any of these tags (exact match)"New value: +"Exclude transaction tag name(s) — drops rows carrying any of these normalized tags (none-of-any, exact match). Other labels and untagged rows remain; [] excludes nothing."
      • changedInput schema / properties / limit / description
        Previous value: -"Page size in detail mode (default 25) and vested_interest mode (default 50). Ignored in grouped mode — use top there."New value: +"Page size 1–200: detail and tag_report default 25, vested_interest defaults 50. Ignored in grouped mode — use top (1–50, default 50) there."
      • changedInput schema / properties / sort / description
        Previous value: -"Sort field (detail mode): date, -date, amount, -amount. Default -date. A typo is a hard error, not a silent fall back to date."New value: +"Sort field (detail mode): date, -date, amount, -amount. Default -date. Signed amounts: amount puts the most negative outflows first; -amount puts the most positive inflows first. A typo is a hard error, not a silent fall back to date."
      • changedInput schema / properties / tag_report / description
        Previous value: -"Per-tag project/tax ledger: each tag's total, count, date span (first/last), and top categories. A trip/project total across categories. Transactions can carry several tags or none: these totals overlap and exclude untagged transactions. Read each tag on its own; never sum across tags or expect them to reconcile to an overall total. Honors all filters; defaults to all time."New value: +"Per-tag project/tax ledger: each tag's total, count, date span (first/last), and top categories. A trip/project total across categories. Transactions can carry several tags or none: these totals overlap and exclude untagged transactions. Read each tag on its own; never sum across tags or expect them to reconcile to an overall total. Honors ledger filters; defaults to all time. Pages by absolute total descending, then tag: limit 1–200 (default 25), cursor and has_more. Finish when has_more is false for complete tag inventory in the selected payment-date window. coverage states payment_date and range (or all); it excludes untagged rows and prepaid trip charges outside that window, so this is not travel-date coverage. Per-tag totals include each transaction once. Live pagination: restart after concurrent edits."
      • changedInput schema / properties / top / description
        Previous value: -"Limit grouped results to top N by amount; grouped comparisons rank by absolute change and include rows with only prior activity. In the time-series matrix (by + granularity) it caps every section's rows: expense groups past N fold into \"Other\" so groups still sum to the total (over by:tag, whose buckets overlap, the extra tags are dropped, not folded); nest.top caps the items under each group."New value: +"Limit grouped results to top N by amount (1–50, default 50); grouped comparisons rank by absolute change and include rows with only prior activity. In the time-series matrix (by + granularity) it caps every section's rows: expense groups past N fold into \"Other\" so groups still sum to the total (over by:tag, whose buckets overlap, the extra tags are dropped, not folded); nest.top caps the items under each group."
  3. 1 tool update
    • Changedadmin4 fields changed
      • changedInput schema / properties / active / description
        Previous value: -"For action=update on rules: true reactivates, false deactivates. On recurring_series: false marks a real bill ended (history stays linked), true restores a series the user ended or rejected."New value: +"For action=update on rules: true reactivates, false deactivates. On recurring_series: false marks a real bill ended (history stays linked), true explicitly affirms the series as recurring until the user ends or rejects it."
      • addedInput schema / properties / frequency
        Added value: +{
        +  "description": "Recurring series create: REQUIRED cadence, even for one selected charge. Detection preserves this declared cadence.",
        +  "enum": [
        +    "weekly",
        +    "biweekly",
        +    "semimonthly",
        +    "monthly",
        +    "quarterly",
        +    "semiannual",
        +    "annual"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / preview / description
        Previous value: -"Rule create/update: dry-run — return the matched transactions + a token WITHOUT writing. The honest counterpart to a tokenless write (which is rejected, never silently previewed). Any other write given preview:true is refused and writes nothing."New value: +"Rule create/update: return matches and a token without writing; token required to apply. recurring_series create: preview the declaration and estimates without writing; omit preview to apply (no token). Other writes given preview:true are refused."
      • addedInput schema / properties / transaction_ids
        Added value: +{
        +  "description": "Recurring series create: 1–200 selected posted, nonignored transactions in one direction/currency, not already assigned. Name and frequency required. Use annotate set_recurring with recurring_series_id to assign to an existing series.",
        +  "items": {
        +    "format": "uuid",
        +    "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
        +    "type": "string"
        +  },
        +  "maxItems": 200,
        +  "minItems": 1,
        +  "type": "array"
        +}
  4. 1 tool update
    • Changedadmin1 field changed
      • changedInput schema / properties / action / enum
        Previous value: -[
        -  "list",
        -  "create",
        -  "update",
        -  "rename",
        -  "merge",
        -  "merge_cluster",
        -  "delete",
        -  "preview",
        -  "hide",
        -  "unhide",
        -  "sync",
        -  "disconnect",
        -  "add_tag",
        -  "remove_tag",
        -  "set_note",
        -  "remove_note",
        -  "propose",
        -  "upsert",
        -  "get",
        -  "import",
        -  "request_upload",
        -  "confirm",
        -  "reject",
        -  "link",
        -  "export",
        -  "read_text",
        -  "substantiate",
        -  "reconstruct",
        -  "tie_out",
        -  "reconcile",
        -  "verify"
        -]New value: +[
        +  "list",
        +  "create",
        +  "update",
        +  "rename",
        +  "merge",
        +  "merge_cluster",
        +  "delete",
        +  "preview",
        +  "hide",
        +  "unhide",
        +  "sync",
        +  "disconnect",
        +  "add_tag",
        +  "remove_tag",
        +  "set_note",
        +  "remove_note",
        +  "propose",
        +  "upsert",
        +  "get",
        +  "import",
        +  "request_upload",
        +  "confirm",
        +  "reject",
        +  "link",
        +  "unlink",
        +  "export",
        +  "read_text",
        +  "substantiate",
        +  "reconstruct",
        +  "tie_out",
        +  "reconcile",
        +  "verify"
        +]
  5. 1 tool update
    • Changedadmin1 field changed
      • changedInput schema / properties / active / description
        Previous value: -"For action=update on rules and recurring_series: true reactivates, false deactivates."New value: +"For action=update on rules: true reactivates, false deactivates. On recurring_series: false marks a real bill ended (history stays linked), true restores a series the user ended or rejected."
  6. 1 tool update
    • Changedquery1 field changed
      • addedInput schema / properties / recurring_series_id
        Added value: +{
        +  "description": "Only transactions linked to this recurring series ID, from query { recurring: true }. Matches membership, not merchant or series name. Use period:'all' for its full history.",
        +  "format": "uuid",
        +  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
        +  "type": "string"
        +}
  7. 1 tool update
    • Changedadmin2 fields changed
      • changedInput schema / properties / event / properties / kind / description
        Previous value: -"commitment (what you owe on demand) · contribution (a capital call — pending = the notice, settled = the wire) · distribution · return_of_capital (reduces basis) · fee (an out-of-pocket wire NOT already inside a contribution) · conversion · write_off · vest (a tranche, with quantity — one dated in the FUTURE is the schedule: the register derives vested / unvested / next_vest / exercise_cost from them and the strike, and reports unvested as UNKNOWN rather than 0 until a future tranche is on record, so record the whole schedule) · exercise (strike paid, with quantity) · mark (a valuation opinion — needs mark_source; recorded, never summed)."New value: +"commitment (what you owe on demand) · contribution (a capital call — pending = the notice, settled = the wire) · distribution · return_of_capital (reduces basis) · fee (an out-of-pocket wire NOT already inside a contribution — paid-in, but it never draws the commitment down, so a fee charged AGAINST the commitment is recorded as part of that call's contribution) · conversion (with instrument_from / instrument_to and the shares received as quantity; the position's own instrument is not changed by it — update the position too, and the write warns when it still reads the old form) · write_off · vest (a tranche, with quantity — one dated in the FUTURE is the schedule: the register derives vested / unvested / next_vest / exercise_cost from them and the strike, and reports unvested as UNKNOWN rather than 0 until a future tranche is on record, so record the whole schedule) · exercise (strike paid, with quantity) · mark (a valuation opinion — needs mark_source; recorded, never summed)."
      • changedInput schema / properties / events / items / properties / kind / description
        Previous value: -"commitment (what you owe on demand) · contribution (a capital call — pending = the notice, settled = the wire) · distribution · return_of_capital (reduces basis) · fee (an out-of-pocket wire NOT already inside a contribution) · conversion · write_off · vest (a tranche, with quantity — one dated in the FUTURE is the schedule: the register derives vested / unvested / next_vest / exercise_cost from them and the strike, and reports unvested as UNKNOWN rather than 0 until a future tranche is on record, so record the whole schedule) · exercise (strike paid, with quantity) · mark (a valuation opinion — needs mark_source; recorded, never summed)."New value: +"commitment (what you owe on demand) · contribution (a capital call — pending = the notice, settled = the wire) · distribution · return_of_capital (reduces basis) · fee (an out-of-pocket wire NOT already inside a contribution — paid-in, but it never draws the commitment down, so a fee charged AGAINST the commitment is recorded as part of that call's contribution) · conversion (with instrument_from / instrument_to and the shares received as quantity; the position's own instrument is not changed by it — update the position too, and the write warns when it still reads the old form) · write_off · vest (a tranche, with quantity — one dated in the FUTURE is the schedule: the register derives vested / unvested / next_vest / exercise_cost from them and the strike, and reports unvested as UNKNOWN rather than 0 until a future tranche is on record, so record the whole schedule) · exercise (strike paid, with quantity) · mark (a valuation opinion — needs mark_source; recorded, never summed)."
  8. 1 tool update
    • Changedadmin2 fields changed
      • addedInput schema / properties / match_trace
        Added value: +{
        +  "description": "Document tie_out ONLY: inspect bounded match references for independently imported rows and Plaid separately. No amounts/memos returned or stored. limit caps total trace rows (default 50, max 500); total/truncated report omitted rows. References reflect this observation, not a frozen replay.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / trace_offset
        Added value: +{
        +  "description": "Document tie_out with match_trace:true: skip this many trace rows (default 0). Follow nextOffset; compare source/input and ledger fingerprints between calls because results are live, not a snapshot.",
        +  "maximum": 80000,
        +  "minimum": 0,
        +  "type": "integer"
        +}
  9. 1 tool update
    • Changedadmin1 field changed
      • addedInput schema / properties / with_download
        Added value: +{
        +  "description": "Document/import get ONLY: opt in to a download object for one stored original (GET URL, headers, expiry, safe filename/content type, exact byte_count and sha256). A host downloads and verifies it to a caller-chosen local path; never put bytes/base64 into context. Import id is the native source id (the upload_id from staging). URLs expire after five minutes; request again for the same id after expiry.",
        +  "type": "boolean"
        +}
  10. 1 tool update
    • Changedadmin1 field changed
      • changedInput schema / properties / filename / description
        Previous value: -"request_upload: original filename (its extension picks the suggested content type — .ofx/.qfx/.qbo vs .csv — and it labels the stored file)."New value: +"request_upload: original filename, the only required file metadata. Import extensions pick the transport content type (.ofx/.qfx/.qbo vs .csv); documents use a binary transport type and recognize PDF/JPEG/PNG bytes on receipt. It labels the stored file."
  11. 2 tool updates
    • Changedadmin2 fields changed
      • changedInput schema / properties / include_hidden / description
        Previous value: -"Include hidden accounts in list (excluded by default)"New value: +"Include hidden accounts in account lists and their transactions in category/party usage n (excluded by default). Party membership and lifetime eff/auto are unchanged."
      • changedInput schema / properties / period / description
        Previous value: -"Period for list counts"New value: +"Period for category/party list usage n (default this_month). Party eff/auto and tag n/provenance remain lifetime counts."
    • Changedquery4 fields changed
      • changedInput schema / properties / changed_since / description
        Previous value: -"vested_interest: only positions with something RECORDED on or after this — an event recorded, or the position created or updated — each row carrying `changes` (the events with when they were recorded, and created / updated flags). A YYYY-MM-DD date (or a time with no offset) is read as UTC; the response echoes as_of (ISO) to pass back next time for an exact cutoff — but only when the whole matched set came back, so page to the end (has_more absent) before storing it. Limits: pages read live position state and return a completion-time watermark, so concurrent records or updates can be missed during pagination. A position field change shows as updated:true without a diff, and an event edited or deleted after the cutoff is invisible (events carry only a recorded time)."New value: +"vested_interest: latest currently matching positions with something RECORDED on or after this (inclusive): an event or position creation/update, with changes. Date and naive ISO mean UTC; offset timestamps keep their exact instant; at most 3 fractional digits and normalized UTC year 0000–9999. Page to completion before saving as_of, the FIRST request clock carried in a scoped versioned cursor. Keep changed_since and book filters unchanged; legacy changed-feed cursors require restarting without cursor from the original cutoff. Next review from as_of intentionally overlaps concurrent records: replace/upsert by position id, never add totals across pages/runs. Membership, folds, totals and alerts stay live; rollups have no feed cursor/as_of. Future row cutoff completes empty at the current clock. Timestamp order is not commit order: earlier-stamped late commits/backward clocks, event edits/deletes, position deletions and filter exits have no guaranteed notifications; position edits coalesce to updated:true without a diff."
      • changedInput schema / properties / health / description
        Previous value: -"Data quality dashboard: unnamed accounts, uncategorized transactions, suspected misclassified income, orphan parties, empty categories, stale recurring"New value: +"Data quality dashboard: accounts needing readable names, uncategorized transactions, suspected misclassified income, orphan parties, stale recurring"
      • changedInput schema / properties / reconcile / description
        Previous value: -"Attach a reconciliation bridge to summary or time-series OUTPUT: as_booked vs adjusted totals plus the per-entry net deltas. A modifier, not a mode of its own — it adds to whatever the other params select, and is scoped by period/start/end like any other query (never by `year`). Works with either view; vs_prior follows `view`, not the bridge."New value: +"Attach a reconciliation bridge to summary or time-series OUTPUT: recorded as_booked activity vs adjusted reporting totals plus each entry's net effect within the exact selected dates. A modifier, not a mode of its own — it adds to whatever the other params select, and is scoped by period/start/end like any other query (never by `year`). Works with either view; vs_prior follows `view`, not the bridge."
      • changedInput schema / properties / view / description
        Previous value: -"Reporting view (summary, grouped, time-series). 'as_booked' (default; alias 'actual') = raw actuals; 'adjusted' = with the adjustment overlay folded into the headline totals, and into the prior period under `compare`, so vs_prior compares adjusted to adjusted. category/group filters (and their exclude_*) scope the overlay; any other filter returns as_booked. Manage entries via admin { entity: 'adjustment' }."New value: +"Reporting view (summary, grouped, time-series). 'as_booked' (default; alias 'actual') = recorded activity for cash/statement comparisons with the same account, dates and financial-kind scope. 'adjusted' applies reporting entries without changing those transactions, so it can differ from cash that moved in that range. Period sums use the exact selected dates even when calendar labels are wider. Under `compare`, adjusted compares to adjusted. category/group filters (and their exclude_*) scope the overlay; any other filter returns as_booked. Manage entries via admin { entity: 'adjustment' }."
  12. 1 tool update
    • Changedquery2 fields changed
      • changedInput schema / properties / by / description
        Previous value: -"Aggregate by dimension(s). Triggers grouped mode. category_tag groups by canonical category tag (essential / discretionary / untagged); most useful with type=expense. country groups by merchant country (ISO alpha-2), city by merchant city; rows with none land in \"Unknown\"."New value: +"Aggregate by dimension(s). Triggers grouped mode. category/group put each selected transaction in one bucket, including Uncategorized; their full buckets add up before top truncation. Any grouping containing transaction tag has overlapping buckets: a transaction can carry several tags or none. Read each tag independently; never sum across tags. Grouped tag results omit total/pct and can include Untagged; tag_report gives standalone per-tag figures without Untagged. category_tag is different: its canonical essential / discretionary / untagged buckets do partition the selected transactions; most useful with type=expense. country groups by merchant country (ISO alpha-2), city by merchant city; rows with none land in \"Unknown\"."
      • changedInput schema / properties / tag_report / description
        Previous value: -"Per-tag project/tax ledger: each tag's total, count, date span (first/last), and top categories. A trip/project total across categories. Honors all filters; defaults to all time."New value: +"Per-tag project/tax ledger: each tag's total, count, date span (first/last), and top categories. A trip/project total across categories. Transactions can carry several tags or none: these totals overlap and exclude untagged transactions. Read each tag on its own; never sum across tags or expect them to reconcile to an overall total. Honors all filters; defaults to all time."
  13. 1 tool update
    • Changedquery1 field changed
      • changedInput schema / properties / compact / description
        Previous value: -"vested_interest: omit each position's note from the rows — the review shape, when ten rows would otherwise each carry up to 2,000 characters of note."New value: +"vested_interest: omit position notes, term notes and mark basis explanations from the rows. Keeps exact term values and their basis/document/page provenance, plus mark amounts, dates and sources. Opt-in, including with changed_since; use smaller limit pages for large books. Full prose remains available through compact:false or admin position get."
  14. 1 tool update
    • Changedadmin1 field changed
      • changedInput schema / properties / set_is_transfer / description
        Previous value: -"Rule action: mark as transfer"New value: +"Rule action: mark/unmark as transfer (true sets, false clears; when applied, overrides automatic transfer classification on future syncs)."

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    B
    maintenance
    Enables AI agents to manage personal finances with deterministic, grounded tools for cash-flow projection, drift detection, reconciliation, and Todoist integration. All figures trace back to a local SQLite database.
    55
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to read, write, and audit personal finance data over a single SQLite file, covering transactions, receipts (including receipt photos as image blocks), budgets, categories, and line-item price tracking. Every write is an idempotent, soft-deleted, audited upsert so agents can safely transcribe and organize spending.
    4 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to manage personal finances through plain-language commands, including expense logging, account and deposit tracking, budget monitoring, and spending reports, with OAuth-secured access to a shared ledger.
    MIT
Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources