spec

Asset Notation Specification - Version 0.4.0

Status: RELEASED. Pre-1.0: the data model is exercised against real data before any v1.0, and a minor version may break; this one does not.

Changes from 0.3.0 (additive, the rest clarifies what 0.3.0 left unsaid):

A document valid under 0.3.0 stays valid here unless it carried a date that does not exist.

1. Purpose and scope

Asset Notation is an open, portable data format for a person’s or household’s assets and liabilities over time: the accounts, positions, properties, debts and their values, across institutions and asset classes, in a single self-describing document that any application can read and write. The consolidated net worth is one thing it makes derivable, but the format itself is the broader notation: it records what you own and owe, not a dashboard.

It exists to fill a concrete gap: today wealth data is locked inside each aggregator and institution, with no neutral interchange format. Asset Notation is the neutral payload. It does not replace bank-messaging standards (ISO 20022) or brokerage feeds; it consolidates their outputs into one portable picture the owner controls.

In scope (v0.4): subjects/owners, institutions, holdings (the single generic asset-or-liability entity), optional instruments for market-priced positions, valuations over time, optional transactions, ownership shares.

Out of scope (v0.4): distribution of market-data and FX rate tables, tax computation, budgeting categories, performance analytics, real-time messaging. These belong in tools or in namespaced extensions, not in the core. A record MAY however capture the single FX rate it was booked at (sections 5.5, 5.6); what stays out of scope is serving rates as data.

2. Design principles

  1. One generic entity. Anything you own or owe is a Holding - a bank account, a savings book, a brokerage position, a house, a car, a mortgage. An open kind says what flavour it is; a nature (asset or liability) says which side of the balance sheet it sits on. There is no separate account / holding / liability hierarchy to learn or to migrate.
  2. Reuse existing identifiers; never invent. Currencies, dates, securities, institutions and accounts are referenced through established ISO standards.
  3. Model facts, not dashboards. The document stores what is true (a holding, a value as of a date). “Net worth at time T” is derived, never stored.
  4. Valuation is first-class, time-stamped and sourced. A value always carries its as-of date and where it came from, because wealth includes illiquid, subjectively-valued assets, and because the value of everything moves.
  5. Money is never a float. Amounts are decimal strings with an explicit currency; quantities and ownership shares are decimal strings too. No accounting figure is ever a binary float.
  6. Forward-compatible and extensible. Unknown fields and unknown kind / source values are preserved, not rejected. App-specific data lives in namespaced extensions, keeping the core small. New kinds and new fields never force a migration.
  7. Privacy by construction. The document is plaintext; confidentiality is the container’s job (section 11). Any subset of a document is still valid, so partial disclosure is natural.
  8. Human-inspectable. Plain JSON: diffable, greppable, hand-editable.

3. Conventions

4. Document structure

A document is a single JSON object:

{
  "assetnotation": "0.4.0",
  "generatedAt": "2026-07-08T09:00:00Z",
  "generator": { "name": "example", "version": "1.0.0" },
  "baseCurrency": "EUR",
  "subjects": [],
  "institutions": [],
  "holdings": [],
  "instruments": [],
  "valuations": [],
  "transactions": [],
  "ownership": [],
  "extensions": {}
}
Field Req Type Notes
assetnotation MUST string Spec version major.minor.patch of this document.
generatedAt MUST timestamp When the document was produced.
generator MAY object { name, version } of the producing app.
baseCurrency SHOULD ISO 4217 Reporting currency for derived totals.
subjects SHOULD Subject[] Owners. If omitted, a single implicit owner is assumed.
institutions MAY Institution[] Banks, brokers, lenders.
holdings MUST Holding[] The assets and liabilities. The heart of the document.
instruments MAY Instrument[] Market-priced securities referenced by holdings.
valuations SHOULD Valuation[] Time-stamped values of holdings.
transactions MAY Transaction[] Optional movement history.
ownership MAY Ownership[] Shares. Defaults to 100% to the primary subject.
extensions MAY object Namespaced app-specific data (section 10).

Collections are unordered sets keyed by id. This maps directly onto a schema-agnostic store of named collections (section 12).

Document integrity. An id MUST be unique within its collection. References (holdingId, parentId, institutionId, instrumentId, subjectId, counterpartHoldingId, a link’s holdingId) SHOULD resolve to an entity in the same document, but MAY dangle: any subset of a valid document is itself valid (section 11), so a partial disclosure can omit a referenced entity. A consumer MUST tolerate an unresolved reference - treat the relation as unknown - and MUST NOT reject the document for it. A chain of parentId MUST be acyclic: a holding is never its own ancestor, since the derivation of section 6 walks the chain and a loop has no root. The reference validator rejects duplicate ids and cycles, and reports unresolved references informatively (section 13).

5. Core entities

5.1 Subject (owner)

A person or legal entity that can own assets and owe liabilities.

Field Req Type Notes
id MUST string  
type MUST enum person, entity.
name SHOULD string Display name.
kind MAY enum e.g. self, spouse, child, company, trust, sci, joint. Open.
country MAY ISO 3166 Tax residence or registration.
ids MAY object e.g. { "lei": "..." } for legal entities.

5.2 Institution

A bank, broker, insurer, exchange, custodian or lender.

Field Req Type Notes
id MUST string  
name SHOULD string  
kind MAY string Open. RECOMMENDED: bank, broker, insurer, exchange, custodian, other. A consumer MUST treat an unknown kind generically.
country MAY ISO 3166  
ids MAY object { "bic"?: ISO 9362, "lei"?: ISO 17442 }.

5.3 Holding (the generic entity)

Anything of value, owned or owed. A bank account, a savings book, a brokerage position, a directly-held house or car, a mortgage or a consumer loan are all Holdings. Its values over time are Valuations (5.5); its movements, if any, are Transactions (5.6).

Field Req Type Notes
id MUST string  
kind MUST string Open: checking, savings, brokerage, retirement, life_insurance, crypto_wallet, cash, real_estate, vehicle, valuable, mortgage, loan, credit_line… A consumer MUST accept an unknown kind and render it generically.
nature MUST enum asset or liability. Drives the sign in net worth.
label SHOULD string Display name.
currency SHOULD ISO 4217 The holding’s native currency.
parentId MAY ref The containing holding (a stock in a fund, a fund in a securities account). A holding with no parent is a root.
institutionId MAY ref -> Institution.
ids MAY object { "iban"?: ISO 13616, "bic"?, "number"? }. number SHOULD be masked.
instrumentId MAY ref -> Instrument, for a market-priced position.
quantity MAY decimal string Units held, when instrumentId is set.
costBasis MAY Money Total acquisition cost of the current position, in any currency. Informative: a tool MAY maintain it from transactions. A corporate action (5.6) changes quantity but leaves costBasis unchanged.
links MAY Link[] Relations to other holdings (below).
attributes MAY object Type-specific scalars the core does not model (a loan’s rate, a property’s address…). Forward-compatible: producers put extra kind-specific data here, not in core fields.
openedAt / closedAt MAY date The holding exists from openedAt inclusive to closedAt exclusive; outside that interval it contributes nothing to a derived total (section 6). A producer SHOULD emit a final valuation at closure (a zero balance, the payoff of a loan), because a consumer that reads the last value alone would otherwise show a balance that no longer exists.

A Link is { "rel": string, "holdingId": ref }. rel is an open vocabulary: funded_by (a card -> its account), financed_by (a property -> its mortgage), held_in (a position -> its account), cash_of (a cash account -> the holding it serves: the settlement cash paired with a securities account, physically separate but logically one envelope). Links express the relationships a 360 view needs without nesting.

Sign convention: a holding’s values are its native signed amounts. An account-style holding carries a signed balance (a negative balance is an overdraft or a card debt, and lowers net worth on its own). A loan-style holding carries a positive outstanding and is marked nature: "liability", so net worth subtracts it. This keeps both representations consistent (section 6). A liability’s values MUST be the outstanding amount as stated by the institution (typically positive): a producer MUST NOT negate a value to express what nature already expresses, since the double negative would flip the sign back.

5.4 Instrument (optional)

A market-priced thing referenced by a holding, so the shared price lives once. Only needed for securities and similar; a house is just a holding with a value.

Field Req Type Notes
id MUST string  
name SHOULD string  
kind MUST string Open. RECOMMENDED: security, cash, crypto, commodity, other. A consumer MUST treat an unknown kind generically.
ids MAY object { "isin"?: ISO 6166, "figi"?, "ticker"?, "mic"?: ISO 10383 }.
currency MAY ISO 4217 Pricing currency.

5.5 Valuation

The value of a holding, as of a date, from a stated source. Valuations form a time series; multiple per holding are expected (a statement each month, an appraisal each year, the amortized balance of a loan over its life).

Field Req Type Notes
id MUST string  
holdingId MUST ref -> Holding.
asOf MUST date/timestamp When the value held true.
value MUST Money The total value of the holding (signed per 5.3).
unitPrice MAY Money For an instrument-priced holding: price per unit.
quantity MAY decimal string Units held as of asOf, making the valuation a self-sufficient position snapshot.
fx MAY FxRate The conversion rate captured when the record was made (below).
source SHOULD string Open: purchase, statement, market, estimate, appraisal, repayment, manual, derived…
confidence MAY number 0..1. An estimate is softer than a statement.

An FxRate is { "base": ISO 4217, "rate": decimal string }: one unit of the record’s currency equals rate units of base, at the record’s date. It freezes the conversion the producer actually used, so a foreign-currency record keeps its exact counter-value even when historical rate feeds differ or disappear.

When quantity and unitPrice are both present and their product disagrees with value (statement rounding), value takes precedence: it is the institution’s stated total. A valuation whose asOf lies in the future is a projection (a loan’s amortization schedule, a vesting plan), not a fact; a producer SHOULD mark it with a projective source (projected, schedule) so consumers can keep facts and forecasts apart.

5.6 Transaction (optional)

A movement on a holding. Optional: a document MAY carry valuations only. Semantics align loosely with ISO 20022 / OFX.

Field Req Type Notes
id MUST string  
holdingId MUST ref -> Holding.
date MUST date/timestamp The day, or a full RFC 3339 instant when the exact time matters (a trade execution, an on-chain settlement).
amount MUST* Money Signed from the holding’s own perspective, in the direction of its value (below). *Not required when type is corporate_action (below).
type MAY string Open. RECOMMENDED: transfer, trade, fee, dividend, interest, income, expense, corporate_action, other. A consumer MUST treat an unknown type generically.
factor MAY* decimal string Quantity multiplier applied by a corporate action: "2" for a 2-for-1 split, "0.5" for a reverse split, an exchange ratio for a merger. *Required when type is corporate_action.
fx MAY FxRate The conversion rate captured at booking time (5.5).
transferId MAY string Opaque id shared by the legs of one internal movement (below).
description MAY string  
category MAY string Open budgeting category.
counterpartHoldingId MAY ref The other side of the flow: a transfer target, the loan repaid, the asset funded.
instrumentId / quantity MAY ref / decimal string For trades; for a corporate action, the instrument affected (and optionally the resulting instrument of a merger via a new instrumentId on the holding).

Sign. A positive amount raises the holding’s value as section 5.3 states it, a negative one lowers it. On an asset, an inflow is positive and an outflow negative. On a liability, whose value is its positive outstanding, a drawdown or a capitalized interest is positive and a repayment is negative. A repayment of 990 from a current account to a mortgage is therefore -990 on the account and -990 on the mortgage: each leg lowers its own holding’s value, and the owner’s net worth is unchanged.

Corporate actions. A split, reverse split or merger changes the number of units held without any cash moving and without changing the capital invested. It is recorded as a corporate_action transaction carrying a factor and no required amount:

{ "id": "t2", "holdingId": "h3", "date": "2026-06-16", "type": "corporate_action",
  "factor": "2", "description": "2-for-1 stock split" }

After the event the holding’s quantity reflects the new unit count and costBasis is unchanged. An amount MAY still appear (cash in lieu of fractional units). A consumer that ignores corporate actions still derives correct totals from valuations; the event exists so that unit counts, cost basis and per-unit history remain explainable.

Internal movements. When both legs of one internal movement of value appear in the document - the debit and credit of a transfer between two holdings, a trade and its cash settlement - they SHOULD share a transferId, an opaque id meaningful only within the document. Each leg’s amount is signed from its own holding’s perspective, and the legs of one movement SHOULD net to zero once each leg is signed by its holding’s nature (+1 asset, -1 liability) and converted by any captured FX: that is the net-worth effect of the move, zero by definition. A consumer computing external flows (income, spending, savings rate) SHOULD net or exclude the transactions grouped by a transferId: without pairing, every internal transfer double-counts as both an expense and an income. counterpartHoldingId names the other side; transferId pairs the exact records.

5.7 Ownership (shares)

Who owns how much of what. Enables joint accounts, family consolidation and holding structures. If ownership is omitted, every holding is owned 100% by the primary subject: the first entry of subjects, or the single implicit owner when subjects is omitted too.

Field Req Type Notes
subjectId MUST ref -> Subject.
holdingId MUST ref -> Holding.
share MUST decimal string In [0, 1], e.g. "0.5", "0.333333". Shares of one holding SHOULD sum to <= 1 on any day; exact thirds do not exist in decimal, so a producer SHOULD round down.
from MAY date First day the share holds, inclusive. Absent: since always.
until MAY date Day the share stops holding, exclusive. Absent: still holds.

Ownership is history, like valuations: a share that changes is recorded by closing the old row with until and adding a new one with from, never by rewriting (section 11). Two rows for the same subjectId and holdingId MUST NOT overlap in time; the reference validator rejects an overlap (section 13). The share of a holding owned by a subject as of a date T is the share of the row whose interval contains T, or zero when there is none.

6. Deriving net worth (non-normative)

A standard models facts; tools compute. Net worth for a set of subjects S as of date T:

netWorth(S, T) =
    sum over LEAF holdings h (no holding names h as parentId) of
        sign(h.nature) * ownValue(h, T) * ownedShare(root(h), S, T)
  where ownValue(h, T) = 0 when T < h.openedAt or T >= h.closedAt,
                         else latestValue(h, T);
        sign(asset) = +1, sign(liability) = -1;
        root(h) = the top of h's parentId chain, which is finite (section 4);
        all converted to baseCurrency using an FX rate as of T.

A container (a fund, a securities account) is therefore counted through its leaves, never twice, and each leaf carries its own sign: a margin loan held inside a brokerage account is subtracted, whatever the account’s nature. Ownership is read on the root of the chain, since a position is owned through the account that holds it. A holding that exists at T but has no valuation dated on or before T contributes zero.

latestValue(h, T) is the value of the valuation of h with the greatest asOf <= T. A richer tool MAY refine an account’s value between statements using its transactions; this reference derivation uses valuations only. When a record carries a captured fx (5.5), a tool SHOULD use that rate to convert that record, and an external rate otherwise; rate tables are not distributed by Asset Notation (out of scope). ownedShare(h, S, T) sums, over h’s ownership rows whose subject is in S and whose interval (from inclusive, until exclusive, section 5.7) contains T, the share decimal strings parsed exactly; with no ownership at all, it is 1 for the primary subject. This is non-normative: implementations MAY derive differently, but SHOULD document how.

7. Identifiers and code lists

Concept Standard
Currency ISO 4217
Date / time ISO 8601 / RFC 3339
Country ISO 3166-1 alpha-2
Language (in extensions) BCP 47
Security ISIN (ISO 6166); FIGI, ticker as alternates
Exchange MIC (ISO 10383)
Institution BIC (ISO 9362), LEI (ISO 17442)
Bank account IBAN (ISO 13616)

Producers SHOULD populate ids with whichever external identifiers they have. Consumers MUST NOT assume any ids entry is present.

8. Money representation

Money is { "amount": "<decimal string>", "currency": "<ISO 4217>" }. The decimal string is base-10, optional leading -, . as the decimal separator, no thousands separators, no currency symbol. Rationale: binary floats cannot represent most decimal cash values exactly; ISO 20022 likewise uses decimal.

9. Compatibility and versioning

10. Extensibility

App-specific or domain-specific data that is not part of the core MUST NOT be placed in core fields. Three mechanisms, from most to least scoped:

This keeps the core small and stable while letting tools carry their own state (for example an app’s own categorization rules) without polluting the standard.

11. Security and privacy

Asset Notation documents are plaintext and inherently sensitive. Confidentiality, integrity and tamper-evidence are the responsibility of the container that carries the document (for example an encrypted, authenticated container), not of this schema.

Records are append-only by convention. Past valuations and transactions are facts: a producer SHOULD correct history by adding a record (a new valuation that supersedes by asOf, a corrective transaction), not by rewriting an old one. Rewriting silently changes every view derived from the document; detecting such a rewrite remains the container’s job (versioned or content-addressed storage), not the schema’s.

12. Containers and transport

Asset Notation is a payload schema, independent of how a document is stored or moved. A document MAY live in a plain file, inside a versioned and optionally encrypted container, in a ZIP, or be served over an API; none of that is the schema’s concern.

Because a document is a set of collections keyed by id (subjects, institutions, holdings, valuations, transactions, ownership), it maps one-to-one onto any document or key-value store. A producer whose internal model is Asset-Notation-shaped can therefore emit a conformant document directly from its own state, reconciling entities by id.

13. Validation policy

The JSON Schema (schema/<version>/) is the machine-readable contract. Its strictness is deliberate and follows one rule, so that “valid” means the same thing to every implementation:

The consequence, stated plainly: open validation does not catch a misspelled optional field on an entity (a stray amoutn on a transaction validates as an unknown field and is preserved untouched). The schema guarantees the structure and types of the fields it knows, and the closure of value objects; it does not guarantee that a producer spelled every optional field correctly. Producers are responsible for that, and SHOULD validate their output against both the schema and their own fixtures.

A stricter, closed-world profile (additionalProperties: false everywhere, extensions allowed only through the declared channels) would turn every such typo into an error. It is intentionally not adopted in 0.x: it would reject documents that are valid today (any document carrying an extra field), so it is a breaking change reserved for a future major version. Until then, strictness is exactly this: closed value objects, open entities.

Beyond the schema, the reference validator enforces the document-level invariants a JSON Schema cannot express: a duplicate id within a collection, a parentId chain that loops, and two ownership rows of one subject on one holding whose intervals overlap are errors; an unresolved reference is reported informatively but permitted (section 4), and so are shares of one holding summing above 1 on some day.

14. Conformance

15. Open questions (for v0.4+)

16. Worked example

A household: one person, a checking account with a cash balance, a brokerage position in an ETF, a directly-owned apartment financed by a mortgage, and the month’s mortgage payment recorded on both sides.

{
  "assetnotation": "0.4.0",
  "generatedAt": "2026-07-08T09:00:00Z",
  "generator": { "name": "example", "version": "1.0.0" },
  "baseCurrency": "EUR",
  "subjects": [
    { "id": "s1", "type": "person", "name": "A. Doe", "country": "FR" }
  ],
  "institutions": [
    { "id": "i1", "name": "Example Bank", "kind": "bank", "ids": { "bic": "EXAMFRPPXXX" } },
    { "id": "i2", "name": "Example Broker", "kind": "broker" }
  ],
  "holdings": [
    { "id": "h1", "label": "Current account", "kind": "checking", "nature": "asset", "currency": "EUR", "institutionId": "i1", "ids": { "iban": "FR76 XXXX XXXX XX" } },
    { "id": "h2", "label": "World ETF", "kind": "brokerage", "nature": "asset", "currency": "EUR", "institutionId": "i2", "instrumentId": "n1", "quantity": "40", "costBasis": { "amount": "3936.00", "currency": "EUR" } },
    { "id": "h3", "label": "12 Main St", "kind": "real_estate", "nature": "asset", "currency": "EUR", "links": [{ "rel": "financed_by", "holdingId": "h4" }] },
    { "id": "h4", "label": "Mortgage", "kind": "mortgage", "nature": "liability", "currency": "EUR", "institutionId": "i1", "attributes": { "rate": 1.8, "startDate": "2022-01-01", "endDate": "2042-01-01", "payment": "990.00" } }
  ],
  "instruments": [
    { "id": "n1", "name": "World ETF", "kind": "security", "ids": { "isin": "IE00B4L5Y983" }, "currency": "EUR" }
  ],
  "valuations": [
    { "id": "v1", "holdingId": "h1", "asOf": "2026-06-24", "value": { "amount": "4210.55", "currency": "EUR" }, "source": "statement" },
    { "id": "v2", "holdingId": "h2", "asOf": "2026-06-24", "value": { "amount": "4920.00", "currency": "EUR" }, "unitPrice": { "amount": "123.00", "currency": "EUR" }, "source": "market" },
    { "id": "v3", "holdingId": "h3", "asOf": "2026-06-01", "value": { "amount": "300000.00", "currency": "EUR" }, "source": "estimate", "confidence": 0.5 },
    { "id": "v4", "holdingId": "h4", "asOf": "2026-06-24", "value": { "amount": "176400.00", "currency": "EUR" }, "source": "statement" }
  ],
  "transactions": [
    { "id": "t1", "holdingId": "h1", "date": "2026-06-05", "type": "transfer", "amount": { "amount": "-990.00", "currency": "EUR" }, "counterpartHoldingId": "h4", "transferId": "x1", "description": "Mortgage payment" },
    { "id": "t2", "holdingId": "h4", "date": "2026-06-05", "type": "transfer", "amount": { "amount": "-990.00", "currency": "EUR" }, "counterpartHoldingId": "h1", "transferId": "x1", "description": "Mortgage payment" }
  ],
  "ownership": [
    { "subjectId": "s1", "holdingId": "h1", "share": "1" },
    { "subjectId": "s1", "holdingId": "h2", "share": "1" },
    { "subjectId": "s1", "holdingId": "h3", "share": "1" },
    { "subjectId": "s1", "holdingId": "h4", "share": "1" }
  ]
}

Net worth as of 2026-06-24: 4210.55 + 4920.00 + 300000.00 - 176400.00 = 132730.55 EUR. The payment x1 lowers the account by 990 and the outstanding by 990: signed by nature, (+1)(-990) + (-1)(-990) = 0, the owner’s net worth is unchanged by it. A multi-currency brokerage with a settlement cash account, a captured FX rate, a pair of trade legs sharing a transferId and a stock split is worked through in examples/brokerage.json.