Portal
Documentation: all sections

Sable Private NAV

The NAV is public. The book does not have to be.

An issuer or fund administrator submits a private book — positions, valuations, liabilities, unit supply, and where each figure came from. Sable computes:

…and returns a signed record that verifies through the same public endpoint that checks every Sable receipt. The submitted book is AES-GCM-sealed at rest, opened in-frame only to compute, and never published. Only the configured outputs are.

This is financial infrastructure, not investment advice.

The two halves

Private NAV has a deliberate auth split, and it is the shape of the product:

HalfEndpointsAuth
Write — submit a book, recompute it, publish a recordPOST /v1/nav/calculate, POST /v1/nav/{asset_id}/refresh, POST /v1/nav/{asset_id}/publishsk-sable_ API key
Read — the published NAV, its history, its provenanceGET /v1/nav/{asset_id}, .../history, .../evidencePublic. No account, no credential

An LP, an exchange, or a regulator fetches the NAV and verifies its signature with no relationship to you at all. An unpublished calculation is returned once, to the caller that ran it, and is otherwise invisible: a missing asset, one never calculated, and one calculated but not published all answer 404 identically, so an outsider learns nothing from the difference.

Units: integers, always

Every figure crossing this API is an integer.

There is no floating point anywhere in the calculation. Every division floors, and the record says so ("rounding": "floor"): a position's value floors, and NAV per unit floors. Flooring never overstates what a holder owns, and it is the rounding bias an issuer's counterparties can reasonably accept in a figure the issuer publishes about itself.

Calculating

curl https://api.buildsable.com/v1/nav/calculate \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "asset_id": "va_1a2b3c4d5e6f7890",
  "positions": [
    {
      "label": "T-bill ladder",
      "quantity_micro_units": 1000000000,
      "price_micro_usd_per_unit": 1000000,
      "evidence": {
        "source_kind": "custodian",
        "source_ref": "Custodian statement 2026-09",
        "retrieved_at": "2026-09-06T09:00:00Z",
        "content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
      }
    }
  ],
  "liabilities": [
    { "label": "Management fee accrual", "amount_micro_usd": 400000000 }
  ],
  "supply_micro_units": 1000000000,
  "onchain_supply_micro_units": 1000000000,
  "tolerance_bps": 25,
  "max_age_secs": 86400
}'

The response is the signed record:

{
  "nav_id": "nav_ff89e1008d84453e",
  "asset_id": "va_1a2b3c4d5e6f7890",
  "seq": 1,
  "status": "VERIFIED",
  "receipt": "eyJ2IjoxLCJ0eXBlIjoibmF2LXJlY29yZCIsIm5hdl9pZCI6…",
  "signature": "0x…",
  "signer": "0xa1e5cff305147d761e1b03fc613c123e6359941d",
  "payload": {
    "gross_asset_value_micro_usd": 2000000000,
    "liabilities_micro_usd": 400000000,
    "total_nav_micro_usd": 1600000000,
    "nav_per_unit_micro_usd": 1600000,
    "supply_micro_units": 1000000000,
    "rounding": "floor",
    "methodology_version": "sable-nav/1",
    "verifier_version": "sable-gateway/0.1.0",
    "freshness": { "age_secs": 3600, "max_age_secs": 86400, "fresh": true },
    "reconciliation": { "supply": "VERIFIED", "supply_delta_bps": 0, "…": "…" },
    "evidence_references": [ { "source_kind": "custodian", "…": "…" } ],
    "reasons": []
  },
  "verify": "POST /v1/receipts/verify with {receipt, signature}"
}

Status: the five answers

Every record carries one status, and the worst applicable condition wins — a report that mentions only the mildest problem is a report that hides the others.

StatusMeaningAre there figures?
VERIFIEDEvery material input carried provenance Sable could check, and nothing disagreedYes
PARTIALLY_VERIFIEDComputed, but at least one input rests on the issuer's own declaration, or no independent supply reference was offeredYes
STALEInputs are past the freshness limit. They were used and labelledYes
DISCREPANCYTwo inputs that should agree do not, beyond the toleranceYes, plus the disagreement
UNAVAILABLEA required input was missingNo. None at all.

Three rules follow from that table, and they are the whole point of the feature:

Never fabricate a NAV. When a position has no price, or supply is missing, the record comes back UNAVAILABLE and every numeric field is absent — not zero, not a partial total. The database stores NULL, so a later reader cannot mistake a failure for a zero-value fund.

Never silently substitute an old value. There is no code path that carries a previous price forward into a book that is missing one. The previous record is not even read during a calculation.

Never average away a disagreement. When two price sources for the same position differ beyond tolerance_bps, the primary price stands and the record says DISCREPANCY. Nothing is blended.

Each record also carries reasons — a closed vocabulary of codes (missing_position_price, stale_input, supply_mismatch, unchecked_input, price_source_disagreement, …) so a machine can act on the outcome without parsing prose.

Evidence and provenance

Every material input can carry an evidence block:

{
  "source_kind": "custodian",
  "source_ref": "Custodian statement 2026-09",
  "retrieved_at": "2026-09-06T09:00:00Z",
  "content_sha256": "e3b0c442…"
}

source_kind is one of market_price, custodian, independent_valuation, onchain_balance, issuer_declared, manual_input. The first four are checkable — they name something outside the issuer that could be re-read. The last two are the issuer's own word, and a book resting on them can never be better than PARTIALLY_VERIFIED. "The issuer said so" is not a check, and the record will not call it one.

content_sha256 is a digest of the input as submitted. It never travels as a value, and the underlying statement never reaches Sable at all. What it answers is: is this the same reading you showed me before, and is it the same one the custodian would hand me? — which is the provenance question the privacy contract leaves available.

GET /v1/nav/{asset_id}/evidence publishes those references for a published record. References only: never a price, never a quantity, never a position value.

Freshness and refresh

A book is exactly as current as its stalest input, so freshness is measured against the oldest retrieved_at in the whole submission, not the newest. max_age_secs (default 24 hours) sets the limit.

That is what POST /v1/nav/{asset_id}/refresh is for. It recomputes the stored book against the current clock and appends a new versioned record — so a VERIFIED record becomes an honest STALE one instead of quietly ageing in place. You can also pass a newly observed onchain_supply_micro_units without resubmitting the whole book. A refresh is metered exactly like a calculation.

Reconciliation

Where the asset is tokenized, supply reconciles across:

Underlying NAV
      ↕
Calculated NAV per unit
      ↕
Token/share supply
      ↕
On-chain supply

Pass onchain_supply_micro_units (with its evidence) and Sable compares it against your reported supply within tolerance_bps, reporting a signed drift in basis points. Beyond tolerance is a DISCREPANCY, and the figures are still published alongside it — the disagreement is disclosed, never hidden.

Omitting the on-chain reference is allowed, and it caps the record at PARTIALLY_VERIFIED with the reason no_onchain_supply_reference. No independent check was offered, so none is claimed.

Publishing

Nothing is public until you publish. POST /v1/nav/{asset_id}/publish marks one record readable by anyone at GET /v1/nav/{asset_id} and on the public page at buildsable.com/nav/{asset_id}. It is idempotent, and it is free — the calculation was already paid for, and charging to publish would put a price on an issuer's honesty. Nobody should have a financial reason to sit on a DISCREPANCY.

Records are versioned and never overwritten. GET /v1/nav/{asset_id}/history returns the full published series, newest first, each entry independently signed. Every calculation also appends an event to the asset's Vault hash chain, so the series is tamper-evident and rides the same public anchoring.

What is disclosed, and what is not

By default the record carries the outputs only: gross asset value, liabilities, NAV, NAV per unit, supply, timestamps, statuses, and the evidence references. It does not carry your positions.

Set disclose_components: true and the record additionally publishes a per-position breakdown that sums exactly to gross asset value. Most issuers do not, and the NAV does not require it.

The book is sealed with AES-GCM using the gateway master key. It is kept — a NAV engine cannot recompute or re-attest something it does not hold, and a record that cannot be re-derived is not evidence of anything — but it exists at rest only as ciphertext beside a book_commitment (a sha256 of the canonical submission). That commitment lets you prove later that a given record came from a given book, without revealing any of it.

Privacy modes

privacy_mode accepts standard (the default) and confidential.

standard runs under the ordinary Sable privacy contract: sealed at rest, opened in-frame, never logged.

confidential is config-gated and off by default. Where the operator has not enabled it, a confidential request is refused with 501 — it is never quietly served under the standard contract, because a silent downgrade is exactly the lie the confidential tier exists to make impossible. Where it is enabled, the request additionally requires a live, verified Intel TDX attestation at calculation time and fails closed without one.

Read the attestation block's own scope sentence before quoting it. It states that Sable verified this deployment's confidential backend at calculation time; the NAV arithmetic itself runs in the gateway process, not inside that enclave.

There is no zero-knowledge proof, MPC, or homomorphic encryption anywhere in Private NAV, and none is claimed. The trust model is stated in the signed payload itself.

Webhooks

Subscribe on the webhooks surface:

EventFires when
nav.calculatedAny calculation or refresh completes
nav.publishedA record is made public
nav.staleA calculation came back STALE
nav.discrepancyA calculation came back DISCREPANCY
nav.source_changedThe set of evidence sources differs from the previous record
nav.supply_changedReported supply differs from the previous record

Payloads are content-free: a nav id, an asset id, a sequence, the status, and the reason codes — never a figure. A webhook body travels to a third-party URL, and an unpublished calculation is nobody's business but the issuer's. The subscriber reads the numbers from the published record.

Cost

A calculation (or a refresh) is metered like any other unit of Sable compute: one usage_events row of kind nav_calculation, one idempotent ledger debit, one pre-authorization hold, and every spend cap and the circuit breaker binding as usual. Publishing is free.

The price is set by the operator (SABLE_NAV_MICRO_USD_PER_CALCULATION, default $0.05). Payment never determines whether a result is marked verified. The engine that decides a status takes no cost, no plan and no previous record as an input — it is a pure function of the submitted book and the clock, which is how that guarantee is enforced structurally rather than remembered. Billing can decide whether a calculation runs; it can never decide what the answer is.

Limits

Positions per book500
Liabilities per book200
max_age_secs60 … 31,536,000
tolerance_bps0 … 10,000
Record TTL (expires_at)24 hours

What Private NAV is not