Skip to main content
Unlink splits work between a Client surface (the user’s process, which holds the spending key) and an Engine (the Unlink backend, which builds Groth16 proofs and broadcasts on-chain). This page describes which secrets live where so you can pick the right integration shape — the SDK enforces these boundaries; you can verify the claim by reading the code in protocol/sdk.

Data flow

The two key nodes are grouped by trust profile: the spending key never leaves the client, while the viewing and nullifying keys both cross to the Engine exactly once, during registration.

Where each secret lives

SecretLives inCrosses toNotes
Spending key (EdDSA on BabyJubJub)Client process onlyNever leavesSigns the SigningRequest locally after the SDK verifies the prepared operation intent; the SDK sends only the resulting signature to the Engine.
Viewing + nullifying keys (Ed25519 + hash)Client processEngine, once at registrationBoth sent during the one-time register step. After registration the Engine retains them to decrypt note memos and track which notes are spendable for the user.
Admin API keyServer process (your backend)Engine, on every authenticated requestHeld by createUnlinkAdmin (@unlink-xyz/sdk/admin). Never ship it in a browser bundle — the browser uses createUnlinkClient from @unlink-xyz/sdk/browser, which only ever sees short-lived authorization tokens.
Authorization tokenBrowser session (rotated ~15 min)Engine, on every browser requestMinted by your backend’s /api/unlink/authorization-token route from the admin API key. Scoped to one unlinkAddress.

What the Engine does and doesn’t see

  • Sees: the public Unlink address, the signed SigningRequest, viewing/nullifying keys after registration (so it can decrypt memos and reason about which notes are spendable).
  • Does not see: your spending key. Every authorisation is signed in the client process after the SDK checks the prepared operation against the requested transfer, withdrawal, or execution funding withdrawal. The Engine cannot forge a transfer or withdrawal.
  • Generates: the Groth16 proof. Proof generation is heavy and runs on the Engine for performance; the witness is built from public state plus the client-supplied signature, so the proof is sound without the spending key.
  • Broadcasts via the relayer: gas sponsorship is paid by the relayer using the tenant’s relay balance.

Choosing the right integration shape

  • Browser (non-custodial) — use createUnlinkClient from @unlink-xyz/sdk/browser. The admin API key stays on your backend; the spending key stays in the browser, which only ever holds short-lived authorization tokens.
  • Server / custodial — use createUnlinkClient from @unlink-xyz/sdk/client with a server-held spending key. The admin API key and the spending key live in the same trusted process. You can still route signing to a remote key holder via a signSigningRequest callback so your backend never holds the spending key.
See Custody models for the full comparison.
This page is the integrator-facing trust model. The team-internal spec for auditors (full property list, adversaries, threat model) lives at docs/internal/spec/trust-model.md and is not published.