Skip to content

Manual assets

Not every asset has an API. Home equity, private-company shares, a friend’s IOU, an offline gold position, a property in real estate — Scani tracks these as manual holdings under a synthetic manual institution. They live in the same accounts / holdings / holding_transactions / holding_balance_observations tables as synced positions; the only differences are holdings.source = 'manual' and a NULL externalId. Manual holdings inherit the whole mental model — the ledger, the rollup, vaults, groups, and the holding-inclusion rule.

Three entry points:

  1. Direct UI entry. The user picks a token, an account (or creates a new manual one), and a balance.
  2. Screenshot import. OpenAI Vision parses a portfolio screenshot; the resulting rows land under a manual institution.
  3. CSV / file import. Bulk-load lots of rows at once.

For each manual holding, the system:

  1. Finds or creates a manual institution for the user (this is per-user — different users get different institution rows for their own “Manual” entries, so manual data stays scoped to the user even though institutions is otherwise shared).
  2. Finds or creates an account under that institution, matching by source/metadata when the input identifies one.
  3. Inserts the holding row with source = 'manual' and externalId = NULL.
  4. Optionally appends a kind='opening_balance' or kind='deposit' transaction to seed the ledger at the right starting point.
  5. Appends a source='user-entered' observation with the current balance.

Manual tokens fall into two buckets:

  • Recognised tokens — fiat (EUR, USD), known crypto (BTC, ETH), public equity (AAPL). These get prices from the configured providers via the price graph just like synced holdings.
  • Custom tokensprivate-company or other type tokens that no public provider knows about. Prices are entered manually by the user. Every edit is logged in token_price_edit_history (append-only, with previousPrice, newPrice, editedByUserId, optional reason) so the audit trail survives.

For a Google Sheets-driven workflow, the Google Sheets provider can read a sheet of (token, price, currency) rows on a schedule and write the prices in — useful for portfolios of dozens of private positions that update by spreadsheet.

Manual holdings are never synced by cron. They only change when the user edits them or re-imports. Each user edit appends a 'manual-correction' observation so the historical chart picks up the change at the correct timestamp.

If the user changes the holding’s balance without adding a corresponding transaction (e.g. “I just realised this is actually 1.5 BTC, not 1.0”), the opening-balance reconciliation flow synthesises the implied difference. The user sees this as a note in the data-quality panel; nothing breaks, and the ledger stays consistent.

Everything you can do with synced holdings:

  • Show up in the dashboard total (subject to the inclusion rule).
  • Attach to vaults with percentage splits.
  • Tag with groups.
  • Contribute to the rollup and chart.
  • Earn yield via an APY config.
  • Be hidden or marked inactive.