- The user’s Personal Server registers the record on chain as the owner. Your app never holds the owner key or the scope’s encryption key.
- A write grant carries a
write:prefix and confers no read.write:notes.entrieslets an app write that scope and nothing else. Reading it back needs a separate bare entry in the same grant. See Scope grammar. - Every record carries who wrote it. Your app signs the exact bytes it stored, and the Personal Server keeps that signature with the record, so authorship is verifiable from the record alone.
The flow
You prove control of your key once per session, then write with a bearer token plus a per-write signature. The token authorizes the write; the signature attributes it.Prerequisites
Step 1 — Open a write session
aud, method, uri, bodyHash, iat, exp — and it must carry a grantId claim naming the write grant. Sign it EIP-191 with your builder key. A bearer token cannot open a write session.
scope lists the grant’s write patterns with the write: prefix stripped — exactly what POST /v1/data/:scope accepts under this token.
The handshake checks that your builder address is registered, the grant exists and is not revoked, the grant carries at least one write: entry, the grantee is the handshake signer, and the grantor is this server’s owner. Scope coverage and revocation are checked again on every write, so revoking the grant stops the next write even inside an open session.
A handshake proof is single-use: a proof that already minted a token is refused (WRITE_SESSION_PROOF_REPLAY). Sign a fresh one per handshake.
Step 2 — Write the record
X-Vana-Write-Signature proof attributes it: your builder key signs aud, method, uri, bodyHash and grantId, so a stored record cannot be moved to another scope or relabelled with a different grant and still verify.
The proof imposes three rules:
- The body must be compact JSON — what
JSON.stringifyemits, with no insignificant whitespace. Your app signs the bytes that will be stored, so a pretty-printed body is refused withWRITE_BODY_NOT_CANONICAL. - The signed
uricovers the query string, with parameters sorted by name and then value. Parameter order does not matter; the parameters themselves must match. - Repeat requests need a
nonceclaim. Two identical writes signed in the same second are byte-identical, and the second is refused as a replay (WRITE_ATTRIBUTION_REPLAY). Add anonce— any unique string up to 128 characters, such as a UUID. Each nonce is single-use.
201:
status: "syncing" means the record is stored and its on-chain registration is in progress. See Provenance & verifiability for what lands on chain.
Writing bytes instead of JSON
AnyContent-Type other than JSON stores the payload as an unstructured record:
$binary record — media type, filename, your metadata, size, content hash, content — not the raw bytes, so the same bytes sent as a JSON write and as a binary write do not share a signature. The maximum body size is 50 MB.
What the Personal Server stamps on the record
The Personal Server adds two reserved keys inside the record’sdata:
A request body that includes its own
$writtenBy or $lineage is rejected with 400 INVALID_BODY. Both keys are excluded when the stored bytes are hashed, so a record with lineage verifies the same way as one without.
A third reserved key, $binary, holds the stored form of an unstructured write: media type, filename, your metadata, size, content hash and content. It is returned on a read like any other content.
$writtenBy and $lineage are not returned on a read under a grant, because they would reveal which other sources the user has connected. The owner sees them in their own storage. A grantee reads the derivation through the lineage view, where sources outside the grant are redacted.Using the SDK
401. sessionCoversScope(session, scope) tells you whether a scope is writable under the session before you send anything.
Errors
Limits to design around
- Append-only. A write registers a new version; there is no edit in place. Model a correction as a new version.
- Sessions do not survive a restart. Open a new session on
401. - A write grant is not a read grant. If your app needs to read what it wrote, the same grant must also carry a bare entry for that scope.
- The Personal Server must be reachable. For a user on the web app, that means Vana is open in their browser. Writes to an offline server are not queued.
Related
- Scope grammar — how
write:entries are encoded and why they never confer read - Derivative data — writing a record that carries lineage, and having the Personal Server compute one for you
- Grants & permissions — obtaining the grant this API runs under
- Personal Servers — where the write lands
- Payments & fees — what the escrow settles