Skip to content

tRPC route catalogue

The api and data-provider expose tRPC routers under presentation/. This page enumerates the routers; precise input / output shapes live with the code (packages/business/shared/src/ for the wire DTOs).

Located in apps/backend/api/src/presentation/routers/.

RouterRole
usersProfile, settings, display currency, magic-link / OTP flows.
sessionsSession read / revoke.
agent-tokensPersonal access tokens for the user’s own AI agent: create (shown once), list, revoke, and whether agent access is on. The tokens authenticate the read-only /mcp endpoint.
pushWeb Push subscriptions, one per browser per device: the VAPID public key, subscribe, unsubscribe, and a status that reports whether the SERVER has keys at all — so the client can say “this deployment cannot notify you” rather than showing a toggle that silently does nothing.
accountsList, create, update, hide accounts.
account-typesCatalogue of account types.
institutionsList institutions. Create the synthetic manual institution.
institution-typesCatalogue of institution types.
holdingsCRUD on holdings, set/unset APY config, attach to vault / group.
valued-assetsProperty and vehicles valued by hand: create one from its purchase, add a dated valuation (a same-day correction is a later row), read its valuation history, edit its details. Each write rebuilds the rollup from the day it changed.
tokensSearch tokens, materialise via TokenIdentityService.
transactionsRead the ledger. Filter by date range, kind, account, holding, transfer group, category. setCategory sets or clears the category on up to 500 rows at once (SC-1652).
categoriesA person’s own transaction categories, nested one level (SC-1652): list with per-category counts, create, update, delete (moving its rows to a replacement), and suggest, which creates a starter set only when the person asks.
vaultsCRUD on vaults, attach/detach holdings with percentage splits.
groupsCRUD on groups, attach holdings.
entitiesRead-only archival ownership records and totals. Creation and assignment are retired; account exports preserve historical ownership links.
householdHouseholds (SC-1647): create, invite, previewInvite, accept, revoke, share and unshare one’s own accounts, leave, remove, transferAdmin, setCurrency; mine reads membership. view and history read only the accounts members share, in the household currency. Read access only: no route writes another member’s rows.
portfolioDashboard headline + chart series (reads portfolio_value_daily).
dashboardAggregate dashboard data — composes calls to portfolio + per-scope rollups.
integrationsConnect / disconnect provider integrations (Binance OAuth, exchange API keys, brokerage tokens, wallets). Owns encrypt/decrypt of credentials.
walletAdd on-chain wallets; list the chains that are supported.
screenshotsUpload screenshot to S3, enqueue screenshot-parse job.
file-importUpload CSV / file, enqueue file-import job.
storagePresigned URL minting for direct S3 reads.
paymentsRecurring bills and income: CRUD, pause / end, the occurrence feed (upcoming returns overdue rows too), manual settlement, and suggested recurring payments detected from outflows (list, dismiss, accept).
bill-calendarThe opt-in bills calendar feed (SC-1654): status, enable and rotate (each returns the URL once; only its hash is kept), disable. The URL serves GET /calendar/<token>.ics, public and keyed only by the token.
vendorsWho a payment is with. List, create, rename, and merge two vendors into one.
documentsUpload an invoice, read its parse status, confirm or reject the extraction.
reviewRead-model over everything awaiting the user — pending job results plus pending document extractions.
transfer-reviewOutflows the transfer-linking matcher could not pair: listPending, resolve (paired with a deposit / left_control / untracked), reopen.
balance-gapsBalance changes no transaction explains: listPending (with the count of what was suppressed and why), answer (flow writes a deposit / withdrawal at a date inside the interval, correction a restatement, growth and unknown write nothing).
settlement-answersBalance-gap answers that imported trade settlements now explain: listPending (per holding, each answer full or partial with its remainder), retire (removes the answer’s rows and keeps a copy; asks again when it also moved another holding), keep, undoRetire (puts the rows back with their original ids).
transit-reviewTransfers still in transit 7 days after they left: listDue, candidates (arrivals on the destination up to 10% under, exact refunds to the source), arrived (a shortfall becomes a fee), cameBack, lost (fee or left_control), stillWaiting (asks again in 7 days, books nothing). Every answer that moves money rebuilds history from the day it left.
untracked-arrival-reviewAn outflow answered untracked whose same amount later arrived in a tracked account (within 1%, at most 7 days after, one arrival for one outflow): listDue, confirm (pairs the two and rebuilds history from the day it left), decline (keeps untracked and never offers that arrival for it again).
jobsThe signed-in user’s own jobs: status, list, retry, cancel, remove.
liabilitiesLoan and card terms on a liability account (getTerms, setTerms), and getProjection: the amount owed, the amortization schedule and the payoff computed from them on read. Reads on an asset account answer null.
batch-operationsBatched mutations the SPA uses for bulk edits.
client-errorsEndpoint the SPA posts unhandled-error reports to.
demoPublic and session-free. status answers whether this deployment is the read-only demo (SCANI_DEMO_MODE=1), and on the demo returns the persona’s identity and the signup URL. The SPA asks this before it asks for a session, so it must not need one.
backupscreate queues a backup of the account, uploaded documents and their files included unless they would take it past what a restore accepts; latest names the newest one and how many documents it left out; downloadUrl signs a five-minute download, only for the backup’s owner. One backup per account; temp/ expires it after 30 days. restorable says whether the account is empty, and restore queues a restore of the caller’s own uploaded backup into it.
budget-app-importstargets lists the caller’s accounts, each saying whether an import may land in it (not when a provider, a wallet or a live sync feeds it); start queues the import of the caller’s own uploaded YNAB register, Actual Budget export or Mint export with the account mapping; list names the caller’s uploads; undo queues the removal of one upload’s rows.
exportsrenderPdf typesets a workbook the client already assembled — it reads no holdings of its own, so a statement cannot drift from the CSV beside it; the account name comes from the session, never the input. everything returns the whole account for the “export everything” file.

Auth: every router except the user-facing magic-link entry points and demo requires a Better-Auth session cookie.

On a demo deployment (SCANI_DEMO_MODE=1) every mutation in every router above is refused with FORBIDDEN before its resolver runs, and the identity behind every query is the demo persona rather than a session. See Environment variables.

data-provider (apps/backend/data-provider)

Section titled “data-provider (apps/backend/data-provider)”

Located in apps/backend/data-provider/src/presentation/routers/. Composed in apps/backend/data-provider/src/presentation/router.ts.

RouterAuthRole
processingBearerVersioned processing.v1 pricing, AI and public-wallet operations for Tier 2. Bounded inputs, owner-attributed usage and paid-operation replay protection.
chainsBearerBlockchain balance + transaction reads (Etherscan V2 across EVM chains, Helius for Solana, Bitcoin RPC, Tron, TON, ENS).
tokensBearertokens.search — free-text search fanned out across CoinGecko, DeFiLlama and Finnhub. The api holds no upstream keys, so its own token search calls this for every external result. Also tokens.enrichIdentity for identity lookups.
emailBearer; raw send is internal onlyauth sends fixed self-hosted authentication templates for customer keys. email.send — used by the api to send magic-link / OTP / verification emails. A customer’s Cloud API key gets 403 FORBIDDEN: this is a facade for Scani’s own services, not a product endpoint (SC-585).
storageBearer — internal onlyPresigned URL minting + the rare server-side read path. Used by managed/internal deployments. Tier 1 and Tier 2 API/worker use their own S3 directly. A customer’s Cloud API key gets 403 FORBIDDEN — the bucket is shared and object keys are unprefixed, so reaching these at all is reaching every tenant’s objects (SC-585).
ogBearerOpen Graph metadata fetch (used by the SPA’s link previews).
contactPublicLanding-page contact form: validates a submission, emails support, sends a receipt. Per-IP rate-limited. No bearer (called from the public marketing site).
keysCookieCloud-management surface (CLOUD_MANAGEMENT_ENABLED=true): mint, list, revoke cloud API keys scoped to the authenticated cloud user.
usageCookieCloud-management read-API: per-user / per-tier usage aggregation from cloud_usage_events.

pricing and ai routers stood here until SC-587 (2026-09) deleted them. Their only caller was a mode: 'cloud' provider registry in the api and worker that was built and never adopted, and which went in the same change — so every backend process now calls CoinGecko, Finnhub, DeFiLlama, Frankfurter, Yahoo Finance and OpenAI itself. Nine public procedures were removed with them; they return NOT_FOUND.

Auth column: Bearer = DATA_PROVIDER_API_KEY (every api / worker call). Cookie = Better-Auth session, only available when CLOUD_MANAGEMENT_ENABLED=true and the data-provider is fronted by the cloud-frontend. Public = no auth (rate-limited per-IP).

Input / output shapes are zod schemas in packages/business/shared/src/. Every router’s payload schema is also the source of truth for the SPA’s tRPC client types.

Two gates fire in order:

  1. Bearer / cookie auth. No bearer / cookie → UNAUTHORIZED.
  2. Rate limit. Per-provider rate limiter (@scani/rate-limiter) returns TOO_MANY_REQUESTS when the upstream’s quota is hit. The api retries via BullMQ’s retry policy.

A missing pricing or chain key is not a gate. A call whose provider has no key is not refused: the provider degrades and the call succeeds with less in it. AI parsing is the exception. screenshots.parseScreenshots, documents.enqueueParse and documents.reparse refuse with PRECONDITION_FAILED when no AI provider can take the file, before anything is queued. Statement files and manual entry keep working. See Optional integration keys.

  1. Create the router file in the appropriate src/presentation/routers/ directory.
  2. Define the input / output zod schema in packages/business/shared/src/.
  3. Register the router in the app’s root router (index.ts alongside the existing routers).
  4. The SPA’s tRPC client picks it up automatically via the end-to-end-typed client factory.