Access rules
A grant on a derivative gives no access to its sources, and a grant on a source gives no access to its derivatives. Grant entries are matched against the requested scope verbatim, so a grant oncoach.weekly never satisfies a read of oura.sleep, and an ungranted scope is refused before any data is looked up.
A derived scope must not share its first dot-segment with any of its sources. A grant on chatgpt.* covers every scope under chatgpt., so a derivative of chatgpt.conversations named chatgpt.summary would be readable under a source grant, and the reverse. The write is refused with 400 LINEAGE_SCOPE_UNDER_SOURCE_PREFIX. Put derivatives in your own namespace: coach.weekly, not oura.weekly.
Three ways to produce one
Use a consent-time question unless your product needs the raw records.
Consent-time questions
The question travels inside the access request. Vana’s consent screen shows it verbatim, names the sources in plain words, and states that the app will not see them. When the user approves, Vana registers the question on the user’s Personal Server as the owner and issues a grant that covers only the derived scope. Your app needs no write access and never calls the Personal Server about the question.400:
Then poll the request status. Once it is
approved, read the derived scope from the returned personalServerUrl under the returned grantId, with the signed read described in Grants & permissions.
App-registered questions
Your app can register a question itself over a write session. The routes live under/v1/derivatives and use the same authorization as a write to the derived scope: the write-session bearer token plus the X-Vana-Write-Signature proof.
The grant this needs
Three kinds of entry, in one grant:coach.weekly entry: the question registers and computes, and then reading the answer fails with SCOPE_MISMATCH, because write:coach.weekly grants no read. Wildcard patterns work in all three entry types.
For an app-registered question, the grant is what limits access, not the question. A question can ask for source content verbatim, so this grant is effectively read access to every source. If the grant does not cover a source scope with a bare read entry, registration is refused with 403 DERIVATIVE_SOURCE_NOT_GRANTED. The same check runs against the live grant before every compute, so narrowing the grant later stops future computes.
The routes
Registration body:
question is 1 to 8000 characters, sourceScopes is 1 to 16, and the registration body is capped at 16 KB. A registration that would make the derived scope a transitive source of itself is refused with 409 DERIVATIVE_CYCLE. Sources do not have to hold data yet: the question computes once they do.
Two signing rules apply on these routes. The signed
uri covers the query string (the list route authorizes you against ?derivedScope=), and repeated polls need a nonce claim in the signed payload, because two identical GETs signed in the same second are refused as a replay. The SDK handles both.Declaring the answer’s shape
Without a declared shape the answer is free text, limited only by the token cap, and can contain source content verbatim. A registration can instead declare the fields its answer is made of. The Personal Server then instructs the model to answer in that shape and validates the reply before writing anything, so a field declared as an integer between 1 and 5 is always one.string, number, integer, boolean or enum, with no nesting. A field declaration may only use the keys listed for its type. Limits: 1 to 16 fields, a field name of 1 to 64 characters matching [A-Za-z][A-Za-z0-9_]*, maxLength 1 to 4000 on a string, 1 to 32 distinct enum values. required defaults to true.
Fields the model returns that were not declared are dropped and never stored. A reply that violates a declared field gets one corrective retry; if the retry also fails, the compute fails and no answer is written. Every recompute is validated against the same shape.
The SDK’s
registerQuestion sends derivedScope, sourceScopes, question and model only. To set answerShape or recompute on an app-registered question, send the registration request directly. A consent-time question accepts recompute through the SDK; answerShape is not available for consent-time questions.Writing a derivative you computed yourself
A result computed outside Vana is written back through the Write API with alineage field: the source data point ids, at most 256, all belonging to the same owner.
keccak256(abi.encode(address owner, string scope)) and is the same for every version of that scope. Lineage therefore points at a data point, not a version: “computed from the owner’s oura.sleep data point”. It is recorded per version of the derived record and is immutable once registered. [] states that the record has no sources; omitting lineage makes no statement.
Because the stamped lineage lives inside the record’s data, the on-chain dataHash commits to it. Changing the lineage changes the hash.
Watching a question
A404 on the derived scope can mean the compute is running, it failed and will be retried, or it failed permanently. The status route tells these apart. It is authorized like a read (a live grant covering the derived scope, or the owner), so an app holding only the consent-time bare read entry can call it:
402 here. status is pending, ready, stale or failed. errorCode is one of inference_unavailable, source_missing, grant_invalid or internal, and is null unless the status is failed.
Poll at the interval in retryAfterSeconds. failed with a number means the Personal Server will retry. failed with retryAfterSeconds: null is permanent: stop polling and tell the user.
404, the user’s Personal Server does not support status yet; fall back to polling the derived scope.
When answers are computed
A source refresh marks the answer stale but does not recompute it. The recompute runs the next time the answer is requested: a read of the derived scope, a status poll, or an explicit recompute.- Authorization runs first, so a refused request never starts a compute.
- One compute runs per question at a time, however many readers ask.
- Reads are not blocked by a recompute: a read returns the stored version while the recompute runs.
- A question registered with
recompute: "snapshot"is never marked stale by a source change.
The answer record
version is the envelope format, not the data point version. answer is always a string. answerData is present only for a question that declared an answerShape, and is the authoritative value when present. inference carries the provider receipt when one was returned. The record does not say who registered the question; that is registeredBy on the registration view.
A record read under a grant does not include the reserved keys $writtenBy and $lineage (see Write API). Read the derivation through the lineage view instead.
On mainnet, a new answer is not payable immediately. It becomes payable once its registration is confirmed, usually within a minute. Until then a read returns
402 that you cannot settle yet; keep polling at your “not ready” interval. A 402 that never clears usually means your escrow is empty: check your balance. See Payments & fees.Reading lineage
Two views of the same graph, both authorized like a read and both free:uri, and the grant is the signed grantId claim. A signature for /lineage/2 is refused on /lineage/3, and cannot be reused under another grant.
{ "redacted": true }, with no id, scope or version, because the id would reveal the scope. Order and count are preserved, and the gateway signs the view it served, so un-redacting or dropping a node invalidates the proof. A source that no longer resolves has version: "0".
Keep redacted nodes and their positions when you render a lineage graph. The graph still shows how many data points the answer came from, in which order and at which versions.
Lineage is served by the gateway, so a derivative’s lineage is readable once its registration has synced. getLineage in @opendatalabs/vana-sdk reads either view.
Where the compute runs
The Personal Server builds the prompt locally and sends it to a confidential inference provider. For each source scope it takes the newest local version, keeps the newest 50 items of each array in a record, removes the reserved keys ($lineage, $writtenBy, $binary), and sends one section per source along with the question. A binary record is represented by a placeholder; its bytes are never sent. The default model is z-ai/glm-5.3-flash.
By default the prompt and the answer are end-to-end encrypted to the Phala confidential inference service with the E2EE v2 protocol, under a key fetched from an attested keyset and verified before use. A Vana relay forwards the ciphertext and cannot read the user’s data, the question or the answer. The relay does see the model name, the number and size of the messages, timing, and the response receipt headers.
Source data leaves the Personal Server only through that encrypted call. A failed compute stores an error class only, never the prompt or the answer. For jobs over data pooled from many users, see Confidential compute.
Errors
Handle
404 NOT_FOUND and a question mismatch as the same “not ready” state, inside one bounded poll loop. Do not retry SCOPE_MISMATCH: the grant does not cover the read. Treat an unreachable Personal Server as the user being offline, not as a failure.
Related
- Write API — the session and the signed write a derivative is stored through
- Scope grammar — why a
write:entry never satisfies a read - Grants & permissions — obtaining the grant, and the access-request flow questions ride in
- Provenance & verifiability — what the chain commits to
- Confidential compute — jobs over pooled data from many users
- Payments & fees — what a read of an answer settles