Skip to main content

Sahidha Agent Guide

This document is for AI agents (Claude, ChatGPT, Gemini, others) — or humans wiring up an agent — that want to operate Sahidha programmatically.

If you're a human looking for the web UI, ignore this and use https://app.sahidha.com. If you're an agent and you stop reading after the first three sections, that's still enough to do useful work.

note

The REST API at https://api.sahidha.com/api/v1 is live now and is the source of truth — see the API reference. The @sahidha/mcp and @sahidha/cli npm packages are in private beta (not yet on the public npm registry); the install commands below are how they'll work once published.

Architecture: one API, two optional skins​

┌──────────────────────┐
│ Fastify REST API │ ← source of truth
│ /api/v1/… (66 eps) │ (RLS, audit, scopes)
└───────────┬──────────┘
│ Bearer ffp_…
┌─────────────────┴──────────────────┐
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ ff CLI │ │ MCP server │
│ apps/cli/ │ │ apps/mcp/ │
│ (humans) │ │ (AI agents) │
└───────────────┘ └────────────────────┘
shell, table, stdio JSON-RPC,
piped JSON typed tool schemas

Pick the right surface:

  • MCP server (apps/mcp/) → if you're an agent inside Claude Desktop, Cursor, Continue, Cline, or any MCP-aware host. Tools are typed, validated, discoverable. This is the recommended path for LLMs.
  • ff CLI (apps/cli/) → if you're a human at a terminal, or an agent with only shell access. Same capability set, friendlier for piping.
  • Direct REST (/api/v1) → if you're building your own integration (mobile app, custom dashboard). Authenticate with the same ffp_… token.

The REST API is the truth. Both skins are independent and optional — neither ships logic the other doesn't have. Choose based on your client, not your goals.


1. The shape of the system​

Sahidha is a multi-tenant household-finance app. Every piece of data belongs to a family (the tenant). Inside a family there are members (human users), entity-members (a HUF, business, trust — non-person tenants), and optional cross-family guests (tax advisors who have read access into other families).

Everything else — accounts, transactions, insurance, investments, contacts, todos, reports — lives under a family. Tenant isolation is enforced at the database level via PostgreSQL Row-Level Security; an agent simply cannot read another family's data even if a route forgot to filter (defence in depth).

Roles​

Every login has exactly one role per family. Agents inherit the role of the user whose token they use — there is no separate "agent" role.

RoleSeesCan change
family_adminEverything in the familyEverything, plus member & family administration
memberOnly their own accounts / dataTheir own data; can pay on-behalf for others
guestAll members' data (read-leaning)Add new transactions / imports / todos only — cannot edit or delete anything that already exists

Guest role — applicable rights​

A guest is a read-leaning collaborator — typically a tax advisor or accountant. A guest exists only as a cross-family guest (Settings → Members → Cross-family guests): an admin invites an existing Sahidha user from another family by email; that person keeps their own login and switches into your family from the header chip. (The former in-family "guest member" role has been retired — role=guest on member create/update is now rejected; use the cross-family invite instead.)

A guest CAN:

  • Read everything in the family — every member's accounts, transactions, insurance, investments, transfers, contacts, todos, and all reports (unlike a member, who sees only their own data).
  • Add new transactions (including on-behalf entries) and import bank statements (parse + confirm).
  • Add todos.
  • Mint API tokens scoped to their own (guest) permissions.

A guest CANNOT:

  • Edit or delete any existing transaction or todo (no edit / delete / restore / bulk action / transfer-link).
  • Create, edit, or delete accounts, insurance policies, or investments.
  • Create or delete transfers, or link transfers / masters.
  • Modify contacts, or touch cross-payments / IOUs.
  • Excel export or import.
  • Open Settings — member management, family settings, categories, or any other admin-only route.

In short: a guest can contribute new entries and read the whole family's finances, but cannot alter or remove existing records or change any structure.


2. Quick start (agents — MCP)​

The MCP server is published to npm as @sahidha/mcp. Hosts that speak MCP (Claude Desktop, Cursor, Continue, Cline) auto-discover and call its 66 tools.

Wire it into your host's config. For Claude Desktop, edit %APPDATA%\Claude\claude_desktop_config.json (Windows) or ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

{
"mcpServers": {
"sahidha": {
"command": "npx",
"args": ["-y", "@sahidha/mcp"],
"env": {
"SAHIDHA_API_URL": "https://api.sahidha.com/api/v1",
"SAHIDHA_TOKEN": "sa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}

npx -y fetches the package on first launch and caches it; nothing to clone, nothing to build. For Cursor (~/.cursor/mcp.json) and Continue the shape is the same. Restart the host; you should see tools named sa_whoami, sa_accounts_list, sa_txns_create, etc.

Get a token from the web UI (Settings → API Tokens → New token, scope=write) or programmatically:

# Mint a fresh token for an agent (CLI, but the resulting token works for MCP).
ff auth login # one-time human login
ff tokens create --name "claude-desktop" --scope write --json
# → { "token": "ffp_...", … } ← capture once; never shown again

Verify tool surface from a host's tool picker — you should see 66 tools.

Pre-publish (workspace) alternative: if you're running from a git clone before the npm publish has landed, use the local path:

{
"command": "node",
"args": ["C:/path/to/Sahidha/apps/mcp/bin/sahidha-mcp.mjs"]
}

Hosted connector (Claude custom connector, read-only)​

If you use Claude (web or desktop) and just want to read your family's finances, you don't need to install anything. Add the hosted connector:

Claude → Settings → Connectors → Add custom connector → URL https://mcp.sahidha.com/mcp

Claude walks you through an OAuth sign-in (your normal Sahidha email + password, or Google). It then has read-only access — the list and report tools only (including sa_reports_capital_gains and sa_valuations_list), and nothing that can create, edit, or delete. Two independent layers enforce that: only GET-equivalent tools are exposed, and the token the connector mints for itself is read-scoped. You can revoke it any time from Settings → API Tokens (it's named claude-connector).

Use the hosted connector for read-only Claude access; use the stdio @sahidha/mcp server below when you need full read/write from Claude Desktop, Cursor, Continue, or Cline with your own token.

3. Quick start (humans — CLI)​

# Install globally — one command, no clone.
npm install -g @sahidha/cli

# Interactive login → mints + stores a token in ~/.sahidha/config.json (0600).
# Point at the hosted API (the default is localhost for local dev).
ff auth login --api https://api.sahidha.com/api/v1

# Smoke-test.
ff auth whoami --json
ff accounts list --json
ff txns list --from 2026-04-01 --to 2026-05-31 --json

For agents using the CLI fallback — always pass --json. The table view is lossy.

The CLI honours these env vars (override the config file):

Env varMeaning
SAHIDHA_API_URLBase URL (default http://localhost:3000/api/v1)
SAHIDHA_TOKENBearer token (replaces config-file value)
SAHIDHA_CONFIG_DIROverride ~/.sahidha location
FF_OUTPUT=jsonForce JSON output, equivalent to --json

4. Authentication​

3.1 How to get a token​

Three ways, pick the one that fits your situation:

  1. Interactive — ff auth login prompts for email + password, mints a token, stores it locally. Best for one-shot human-driven setup.
  2. Web UI — sign in as a family_admin or member, open Settings → API Tokens, click "New token", choose scope, copy the plaintext (shown once). Best for granting an agent its own dedicated key.
  3. Programmatic — for tests or onboarding flows, you can POST to /auth/login yourself to get a session cookie, then POST /me/tokens to mint. See src/login.ts in the CLI for the exact pattern.

3.2 Token format and storage​

  • Format: ffp_<43-char-base64url> (ffp = Sahidha Personal). Total length 47 chars. Grep-able prefix is intentional so leaked tokens are easy to find.
  • Stored server-side as SHA-256 hash. The plaintext is shown once at creation; if you lose it, revoke and mint a new one.
  • Scopes:
    • read — GET requests only. Read-only audits, reports, exports.
    • write — GET + mutations the issuing user can perform.
    • admin — Same as write but the issuer must be family_admin at mint time. Allows admin-only routes (member CRUD, audit truncate, etc.).
  • Expiry: optional. Pass --expires-days N at creation. No expiry by default — be deliberate about this for shared/agent keys.

3.3 Using the token​

Pass it as a Bearer header to the API directly:

GET /api/v1/me HTTP/1.1
Authorization: Bearer ffp_abc123...

Or via the CLI, which adds the header for you (after ff auth login).

3.4 Revoking​

ff tokens list --json # find the id
ff tokens revoke <id> # sets revokedAt; future calls 401

Or revoke from the web UI. Revocation is immediate — no propagation delay.

3.5 Multiple families​

A user with cross-family guest memberships has one active family per session. Each API token is pinned to a single family at mint time and cannot switch. To act in a different family:

  1. Switch the web UI to that family (header chip).
  2. Mint a new token there.
  3. Use both tokens, one per family, in your agent.

5. CLI reference (every endpoint)​

See ff --help and ff <cmd> --help for the up-to-date list. Below is the full surface, grouped by resource.

Naming conventions:

  • All flags are long-form (--foo value or --foo=value); boolean flags are --foo (true) or --no-foo (false).
  • <positional> is required, [--opt] is optional.
  • IDs are UUIDs unless noted. Dates are YYYY-MM-DD.
  • Amounts are positive numbers in INR. Sign is determined by --type.

4.1 Auth & tokens​

ff auth login [--email E] [--password P] [--token ffp_…] [--api URL] [--name LABEL]
ff auth logout
ff auth whoami
ff auth config

ff tokens list
ff tokens create --name LABEL [--scope read|write|admin] [--expires-days N]
ff tokens revoke <id>

4.2 Me (current user)​

ff me get
ff me password --current PW --new PW
ff me families
ff me set-active-family --family-id <uuid> # cookie sessions only; tokens are pinned

4.3 Family (admin)​

ff family update [--name S] [--fy-start 1..12]
ff family members list
ff family members create --name N --email E --password P
[--role family_admin|member]
[--entity-type huf|business|trust|partnership|proprietorship|other]
[--managed-by <userId>]
ff family members update <id> [--role …] [--active true|false] [--password …]
ff family members delete <id> # blocked if owns data
ff family guests list
ff family guests invite --email E # email must match an existing user in another family
ff family guests revoke <userId>

4.4 Accounts​

ff accounts list [--owner <userId>]
ff accounts create --name N --type savings|current|overdraft|loan|individual_loan|credit_card|fd|ppf|investment
--bank B [--number LAST4] [--opening N] [--credit-limit N] [--rate N] [--contact <id>]
# --rate (annual interest %) applies only to individual_loan (acquaintance loans)
ff accounts update <id> [--name] [--bank] [--number] [--credit-limit] [--rate] [--owner] [--active] [--contact]
ff accounts delete <id> # blocked if any transactions exist

ff accounts statements <accountId> list
ff accounts statements <accountId> download <archiveId> [--out FILE]
ff accounts statements <accountId> delete <archiveId>

# Per-account visibility grants (app-layer co-ownership; same verbs for insurance & investments)
ff accounts visibility <id> # who can see this account
ff accounts grant <id> --user <userId> # share with a member (full co-ownership)
ff accounts revoke <id> --user <userId>

4.5 Transactions (alias: txns)​

ff txns list [--account <id>] [--owner <userId>]
[--type expense|income|investment|insurance|transfer]
[--category <id|"null"|"any">]
[--from YYYY-MM-DD] [--to YYYY-MM-DD] [--q "search text"]
[--include-deleted] [--deleted-only]
[--link-id <id> --link-type insurance|investment]

ff txns create --account <id> --date YYYY-MM-DD --description S
--type expense|income|investment|insurance|transfer
--amount N
[--category <id>] [--ref S]
[--on-behalf --beneficiary <userId>]
[--link-type insurance|investment --link-id <id>]

ff txns update <id> [--description] [--ref] [--category] [--date]
[--debit] [--credit] [--type expense|income]

ff txns delete <id> # soft-delete (transfer pair: hard-delete both legs)
ff txns restore <id> # undo soft-delete
ff txns transfer-candidates <id> [--window-days N]
ff txns link-transfer <id> --counterparty <txnId>
ff txns convert-to-transfer <id> --to-account <accountId> # turn a lone txn into a transfer leg
ff txns unlink-transfer <id> # split a transfer back into a plain txn
ff txns link-master <id> --type investment|insurance --id <masterId>

ff txns bulk --action recategorize|delete --ids id1,id2,id3 [--category <id|"null">]

4.6 Categories (admin for writes)​

ff categories list # active + inactive, parent + children
ff categories create --name N --kind income|expense [--itr HEAD] [--parent <id>]
ff categories update <id> [--name] [--itr] [--parent] [--active true|false]
ff categories delete <id> # soft-deletes if any txn uses it

4.6a Categorization rules (alias: rules)​

Auto-categorize transactions by merchant/description pattern. suggest proposes rules from your existing categorized history; generate applies the ruleset to currently-uncategorized transactions.

ff categorization-rules list
ff categorization-rules create --match "AMAZON" --category <id> [--type expense|income]
ff categorization-rules update <id> [--match] [--category] [--active true|false]
ff categorization-rules delete <id>
ff categorization-rules suggest # propose rules from history
ff categorization-rules generate # apply rules to uncategorized txns

4.7 Insurance & attachments​

ff insurance list [--owner <userId>]
ff insurance create --name N --insurer S --type Life|Term|Health|Accident|Home|Motor|Travel
--number S
[--sum-assured N] [--premium N]
[--frequency monthly|quarterly|yearly]
[--next-due YYYY-MM-DD] [--nominee S] [--start YYYY-MM-DD]
[--linked-account <id>] [--contact <id>]
ff insurance update <id> [--name] [--insurer] [--premium] [--next-due] [--owner] …
ff insurance delete <id> # blocked if any txn linked

ff insurance attachments <policyId> list
ff insurance attachments <policyId> upload --file PATH # PDF / image, ≤10 MB
ff insurance attachments <policyId> download <attId> [--out FILE]
ff insurance attachments <policyId> delete <attId>

ff insurance visibility <id> # per-policy grants
ff insurance grant <id> --user <userId>
ff insurance revoke <id> --user <userId>

4.8 Investments​

ff investments list [--owner <userId>]
ff investments create --name N --type Mutual_Fund|Fixed_Deposit|PPF|Stocks|Bonds|NPS|Gold|Real_Estate|Rental_Assets|Vehicle|Machinery|Furniture|Other_Investments
--folio S
[--invested-amount N] [--current-value N] [--monthly-sip N]
[--start YYYY-MM-DD] [--maturity YYYY-MM-DD] [--rate N]
[--linked-account <id>] [--contact <id>]
ff investments update <id> [--name] [--current-value] [--monthly-sip] [--owner] …
ff investments delete <id> # blocked if any txn linked

ff investments visibility <id> # per-holding grants
ff investments grant <id> --user <userId>
ff investments revoke <id> --user <userId>

4.8a Valuations (holdings cost-vs-market)​

Dated valuation entries drive per-FY cost-vs-market accounting for an investment or insurance master. Realised gain on a disposal is the new cost value minus the previous cost value; the per-FY capital-gains report rolls these up.

ff valuations list [--type investment|insurance --id <masterId>]
ff valuations create --type investment|insurance --id <masterId>
--date YYYY-MM-DD --cost-value N --market-value N
ff valuations delete <id>

4.9 Transfers​

ff transfers list
ff transfers create --from <accountId> --to <accountId> --date YYYY-MM-DD --amount N [--note S]
ff transfers delete <groupId> # reverses both legs

4.10 Cross-payments / IOUs (alias: cp)​

ff cross-payments list
ff cross-payments create --beneficiary <userId> --amount N --description S
[--category S] [--date YYYY-MM-DD]
ff cross-payments update <id> [--description] [--category] [--date] [--amount] [--beneficiary] [--payer]
ff cross-payments delete <id>
ff cross-payments acknowledge <id> # called by the beneficiary
ff cross-payments settle <id> --account <settlementAccountId>

4.11 Contacts​

ff contacts list [--kind rm|insurance|investment|tax|legal|medical|contractor|other] [--include-inactive]
ff contacts create --name N [--kind …] [--org] [--phone] [--email] [--address] [--notes]
ff contacts update <id> [--name] [--kind] [--org] [--phone] [--email] [--address] [--notes] [--active]
ff contacts delete <id>

4.12 Todos​

ff todos list [--status open|done] [--assigned-to <userId>]
ff todos create --title T [--notes] [--due YYYY-MM-DD] [--assigned-to <userId>] [--visibility family|private]
ff todos update <id> [--title] [--notes] [--due] [--assigned-to] [--visibility] [--completed true|false]
ff todos delete <id>

4.13 Imports (bank statements)​

ff imports parse --file PATH --type csv|xlsx|pdf
[--password PW] # only for encrypted PDFs
[--account <id>] # required for header-rememberment
[--header-row N]

# The output contains `archiveId`, `headers`, `rows`, and an optional `hint`
# (per-bank column mapping remembered from a prior successful import).
# Persist the rows to disk, edit if needed, then confirm:

ff imports confirm --rows-file PATH.json --account <id>
[--archive-id <id>] # echo from parse output
[--mapping '{"date":"Date","debit":"Withdrawal",…}']
[--swap-direction true|false]
[--header-row N]

Passwords are sent over HTTPS in the body, used once to decrypt, never logged or returned in errors, and never stored.

4.14 Excel​

ff excel export [--out FILE] # downloads all data as one .xlsx
ff excel import --file PATH # uploads + routes rows by sheet name

4.15 Reports​

ff reports income-expenditure [--fy YYYY-YY] [--owner <userId>]
ff reports account-statement --account <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
ff reports credit-cards [--owner <userId>]
ff reports net-worth [--fy YYYY] [--fy-start-month 1..12] # admin-only
ff reports capital-gains [--fy YYYY-YY] [--owner <userId>] # realised gains per FY

4.16 Invitations (admin)​

ff invitations create --email E [--role family_admin|member] # guests: use the cross-family guest invite, not this
# Returns { token, url, expiresAt } — the invitee uses the URL to set their password.

4.17 Audit (admin)​

ff audit export [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--out FILE]
ff audit truncate --before YYYY-MM-DD

4.18 Raw HTTP escape hatch​

For anything not yet wrapped, or for novel orchestration:

ff request GET /reports?type=net-worth
ff request POST /transactions --body '{"accountId":"…","amount":100,…}'
ff request POST /transactions --body-file txn.json

Path prefix /api/v1 is normalised — pass it or omit it, both work.


6. Common workflows​

5.1 Reconcile a bank statement end-to-end​

# 1. Parse — preview rows; do NOT auto-confirm.
ff imports parse --file ./statement-HDFC-may.pdf --type pdf --account "$ACC" --json > parsed.json

# 2. Inspect. Each row has { date, description, debit, credit, suggestedType }.
jq '.rows[:5]' parsed.json

# 3. Edit if needed (typical: drop a header-page artifact row).
# Then write back the cleaned rows:
jq '.rows' parsed.json > rows.json

# 4. Confirm.
ff imports confirm --rows-file rows.json --account "$ACC" \
--archive-id "$(jq -r .archiveId parsed.json)" --json
# Find the investment id.
INV=$(ff investments list --json | jq -r '.[] | select(.folioNumber=="HDFC123") | .id')

# Add the transaction.
ff txns create --account "$ACC" --date 2026-05-01 \
--description "HDFC Top 100 SIP" --type investment --amount 5000 \
--link-type investment --link-id "$INV" --json

5.3 Pull a quarterly Income & Expenditure summary​

ff reports income-expenditure --fy 2025-26 --json |
jq '{income: .totalIncome, expense: .totalExpense, net: .net}'

5.4 Bulk-recategorise all "Amazon" transactions​

IDS=$(ff txns list --q "amazon" --json | jq -r '[.[].id] | join(",")')
ff txns bulk --action recategorize --ids "$IDS" --category "$SHOPPING_CAT_ID" --json

5.5 Snapshot a family's net worth nightly​

ff reports net-worth --fy 2025 --json >> ~/snapshots/networth-$(date +%F).json

5.6 Record a year-end valuation and read realised gains​

# Find the investment id.
INV=$(ff investments list --json | jq -r '.[] | select(.folioNumber=="HDFC123") | .id')

# Record a dated valuation (cost basis vs market value at FY-end).
ff valuations create --type investment --id "$INV" \
--date 2026-03-31 --cost-value 100000 --market-value 142000 --json

# Realised gains roll-up for the FY.
ff reports capital-gains --fy 2025-26 --json | jq '{realised: .totalRealised, byHolding: .holdings}'

7. JSON I/O shapes​

This section sketches the wire shapes the most-used endpoints. For the rest either use --json and inspect, or read the corresponding route handler in apps/api/src/routes/.

6.1 Common envelope​

Successful responses are always JSON. Failures use the same content-type with HTTP status >=400 and shape:

{ "error": "human-readable message" }

Binary endpoints (/excel/export, /audit/export, attachment downloads, statement-archive downloads) return raw bytes with an application/... content-type plus a Content-Disposition: attachment; filename="…" header. The CLI saves to disk; the API SDK returns a Blob.

6.2 Transaction (read shape)​

{
"id": "uuid",
"accountId": "uuid",
"txnDate": "2026-05-12",
"description": "Salary credit",
"refNumber": null,
"categoryId": "uuid|null",
"entryType": "income",
"debit": 0,
"credit": 250000,
"linkType": null,
"linkId": null,
"crossPaymentId": null,
"transferGroupId": null,
"source": "manual",
"financialYear": "2026-27",
"isReconciled": false,
"isDeleted": false,
"notes": null,
"createdAt": "ISO",
"updatedAt": "ISO"
}

6.3 Account​

{
"id": "uuid",
"name": "HDFC Savings",
"accountType": "savings",
"bankName": "HDFC Bank",
"accountNumberMasked": "****4567",
"ownerUserId": "uuid",
"openingBalance": 100000,
"currentBalance": 152340.50,
"creditLimit": null,
"contactId": "uuid|null",
"isActive": true
}

6.4 Income & Expenditure report​

{
"type": "income-expenditure",
"fy": "2025-26",
"income": { "Salary": [0,0,250000,...], "Other": [...] }, // 12 months, FY order (Apr-start)
"expense": { "Rent": [...], "Groceries": [...] },
"incomeIds": { "Salary": "uuid", "Other": null },
"expenseIds": { "Rent": "uuid", "Groceries": "uuid" },
"totalIncome": 1234567,
"totalExpense": 765432,
"net": 469135
}

6.5 Net worth report​

{
"type": "net-worth",
"assets": 12_345_678,
"liabilities": 234_567,
"netWorth": 12_111_111,
"breakdown": { "bankAssets": 9_000_000, "investmentAssets": 3_000_000, "lifeAssets": 345_678, "liabilities": 234_567 },
"compare": {
"lastFyLabel": "FY 2024-25",
"lastFyEnd": "2025-03-31",
"thisFyLabel": "FY 2025-26",
"thisFyEnd": "2026-03-31",
"bankAssets": { "last": …, "now": …, "diff": … },
"investmentAssets": { "last": …, "now": …, "diff": … },
"lifeAssets": { "last": …, "now": …, "diff": … },
"liabilities": { "last": …, "now": …, "diff": … },
"assets": { "last": …, "now": …, "diff": … },
"netWorth": { "last": …, "now": …, "diff": … },
"snapshotNote": "Investment current value and Life-policy value aren't snapshotted historically; the same value is shown for both periods."
}
}

8. Error model & exit codes​

The CLI maps HTTP status to shell exit code so scripts can branch:

ExitMeaning
0Success
1Generic error / network failure / bad input
2Unknown command / argv parse failure
3401 unauthorised — token missing or invalid
4403 forbidden — wrong role or wrong scope
5404 not found
6409 conflict — duplicate or constraint violation
7429 too many requests (reserved)
85xx server error

With --json, errors are emitted to stderr as { error, status, body }.


9. Safety guardrails for agents​

  1. Always read before you write. Verify the target's state with a list or get before issuing PATCH / DELETE. The API is idempotent for some operations (txns delete on an already-deleted txn is a no-op) but not for others (e.g. transfers create is not — it would create a duplicate).
  2. Never log the token plaintext. Treat SAHIDHA_TOKEN like a password. Logs uploaded to ChatGPT / Claude transcripts are not safe storage.
  3. Don't bulk-delete from a regex. If you're operating on a list, print the candidates first and ask the user to confirm — ff txns list --q … then ff txns bulk --action delete --ids …. Same for accounts.
  4. Watch the FY boundary. Reports and many filters are FY-aware (Apr-start by default; configurable per family via financialYearStartMonth). ff txns list without --from/--to returns only the current FY.
  5. Statement uploads dedup by content hash. Re-uploading the same file does not duplicate transactions — the archive row updates in place and the parse-confirm flow re-checks duplicateHash per transaction.
  6. Imports require human review. Parsed rows aren't transactions yet. Always inspect, edit if necessary, and only then call imports confirm.
  7. Cross-family guest mode is read-leaning. Even with a write token, a guest cannot edit accounts / insurance / investments / settings. Plan agent capabilities around that.
  8. Audit logs are permanent. Every action via API token is logged with the token's name. Mutations cannot be hidden — agents should assume their behaviour is reviewed.
  9. No financial-transaction execution. This app only records money movements. Nothing here initiates a bank transfer or trade. If a user asks an agent to "send the rent," instruct them to do it in their bank app and then record it with ff txns create.

10. MCP tool reference​

The MCP server exposes 66 tools. All names are prefixed sa_ (Sahidha) so they're easy to spot in a host's tool list. Grouped by resource:

GroupTools
identitysa_whoami, sa_families
tokenssa_tokens_list, sa_tokens_create, sa_tokens_revoke
accountssa_accounts_list/create/update/delete, sa_accounts_statements_list/delete
transactionssa_txns_list/create/update/delete/restore, sa_txns_transfer_candidates,
sa_txns_link_transfer, sa_txns_link_master, sa_txns_bulk
categoriessa_categories_list/create/update/delete
insurancesa_insurance_list/create/update/delete, sa_insurance_attachments_list/delete
investmentssa_investments_list/create/update/delete
transferssa_transfers_list/create/delete
cross-paysa_cross_payments_list/create/acknowledge/settle/delete
contactssa_contacts_list/create/update/delete
todossa_todos_list/create/update/delete
importssa_imports_parse, sa_imports_confirm
reportssa_reports_income_expenditure, sa_reports_account_statement,
sa_reports_credit_cards, sa_reports_net_worth, sa_reports_capital_gains
valuationssa_valuations_list, sa_valuations_create, sa_valuations_delete
family/adminsa_family_members_list, sa_family_guests_list/invite/revoke, sa_invitations_create
rawsa_request (escape hatch for any endpoint above)

sa_accounts_create / sa_accounts_update accept the full account-type set including individual_loan (acquaintance loans), and an optional interestRate field that applies only to individual_loan.

Each tool has a JSON Schema for inputs that the host validates BEFORE calling. Look up the exact shape via tools/list, or read apps/mcp/src/tools.ts — each entry is { name, description, inputSchema, run }.

Binary endpoints (excel export, statement download, attachment download) return a binary-descriptor object instead of streaming bytes through the MCP channel — agents that need the actual file should fall back to the CLI or call the REST API directly with the same ffp_… token.


11. Where to look next​

QuestionFile
What endpoints exist?apps/api/src/routes/*.ts
What's the wire shape of X?The route's response-builder code
What does Y validate against?packages/api-contract/src/*.ts (Zod schemas)
How does auth resolve a token?apps/api/src/plugins/auth.ts
How is family scope enforced?packages/db/prisma/rls/*.sql (Row-Level Security)
How do I add a new command to the CLI?Append to the array in apps/cli/src/commands.ts
How do I add a new MCP tool?Append to the array in apps/mcp/src/tools.ts
Why did my mutation fail with 403?Check role + token scope. Run ff auth whoami.

12. Versioning​

This guide is versioned alongside the API. Breaking changes will:

  1. Bump the API to /api/v2 and keep /api/v1 running for two releases.
  2. Update this file in the same commit.
  3. Note the change in CHANGELOG.md (forthcoming).

Until v1.0.0, additive changes only on /api/v1. Token format is stable.