Skip to main content
Status.
  • Shipped in @opendatalabs/personal-server-ts 1.16.0, on both the Node server and the in-browser PS Lite runtime, and in @opendatalabs/vana-sdk 3.23.0.
  • Writes are free. The escrow settles two operation types today, grant and data_access; there is no write or registration fee on either network.
  • The hosted consent screen issues read grants only. A write grant is still arranged with the Vana team rather than self-served.
  • Records are append-only. A write adds a version; it does not edit one.
The Write API stores a record your app produced — a summary, an enrichment, an imported record — in the user’s own Personal Server, where the user can see it, grant it to someone else, and delete it. It is built around one constraint: your app must be able to write a scope without being able to read it, and without holding the user’s key. Three properties follow from that:
  • The user’s Personal Server signs the on-chain registration as the owner. The app never holds the owner key and never touches the encryption key for the scope.
  • The write grant carries a write: prefix and confers no read. write:notes.entries lets 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. The 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 session is authorization; the signature is attribution.

Prerequisites

Step 1 — Open a write session

The payload is the standard Personal Server request-signing envelope — aud, method, uri, bodyHash, iat, exp — and it must carry a grantId claim naming the write grant. It is signed EIP-191 by your builder key. A bearer credential can never open a write session: the handshake exists to prove control of the key.
scope lists the grant’s write patterns with the write: prefix stripped — exactly what POST /v1/data/:scope will accept under this token. What the handshake checks: the builder 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. Per-scope enforcement is not settled here — it runs against the live grant on every write, so a grant revoked mid-session fails the next write, not the next handshake.
Sessions are held in memory. A Personal Server restart — and in PS Lite, closing the hosting tab — drops every live token, and the app re-handshakes. Treat a 401 on a write as “re-open the session and retry once”, which is what the SDK does for you.
The handshake proof is single-use: a still-valid proof cannot mint a second token (WRITE_SESSION_PROOF_REPLAY). Sign a fresh one per handshake.

Step 2 — Write the record

Two credentials, two jobs. The bearer token authorizes the write. The X-Vana-Write-Signature proof attributes it: it is signed by the same builder key over aud, method, uri, bodyHash, grantId — so a stored record cannot be lifted onto another scope, or relabelled with a different grant, and still verify. Three rules the proof imposes:
  • The body must be compact JSON — what JSON.stringify emits, no insignificant whitespace. The app signs the bytes that will be stored, so a pretty-printed body is refused with WRITE_BODY_NOT_CANONICAL rather than stored with an attribution nobody can check.
  • The signed uri covers the query string, canonicalised with parameters sorted by name and then value. Parameter order does not matter; the parameters themselves must match.
  • Repeat requests need a nonce claim. Two identical writes signed in the same second are byte-identical, and the replay guard refuses the second (WRITE_ATTRIBUTION_REPLAY). Add a nonce — any unique string up to 128 characters, a uuid is the intended shape — and each call is distinct. The nonce is then itself single-use.
On success the Personal Server ingests, encrypts, uploads and registers the record through the normal owner path, and answers 201:
status: "syncing" means the record is stored locally and the on-chain registration is in flight. See Provenance & verifiability for what lands on chain.

Writing bytes instead of JSON

Any Content-Type other than JSON stores the payload as an unstructured record:
The signed representation is the stored $binary record — media type, filename, your metadata, size, content hash, content — not the raw bytes, so switching the same bytes between a JSON and a binary write fails verification. The Node server caps an ingest body at 50 MB.

What the Personal Server stamps on the record

The server adds two bookkeeping keys inside the record’s data: A request body that brings its own $writtenBy or $lineage is rejected with 400 INVALID_BODY. Both are stripped before the stored bytes are re-hashed, so a record with lineage verifies exactly like one without. A third reserved key, $binary, is not bookkeeping: it is the stored form of an unstructured write, holding the media type, filename, your metadata, size, content hash and content. It is served on a read like any other content.
$writtenBy and $lineage are not served on a read under a grant. A data point id is keccak256(owner, scope), so a grantee holding one could hash known scope names against the owner’s address and learn which sources that person has connected. The owner reads them from their own storage; a grantee gets the derivation through the lineage view instead, with uncovered nodes redacted.

Using the SDK

The SDK signs the handshake, canonicalises the body, adds the per-write proof and its nonce, and re-opens the session once on a 401. sessionCoversScope(session, scope) answers whether a scope is writable under the session you hold before you send anything.

Errors

Limits to design around

  • Append-only. A write registers a new version; there is no edit in place. Model corrections as a new version, not a mutation.
  • Sessions do not survive a restart. Re-handshake 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 browser-hosted PS Lite that means the user has Vana open. A write cannot be queued for an offline server.