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
| Secret | Lives in | Crosses to | Notes |
|---|---|---|---|
| Spending key (EdDSA on BabyJubJub) | Client process only | Never leaves | Signs 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 process | Engine, once at registration | Both 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 key | Server process (your backend) | Engine, on every authenticated request | Held 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 token | Browser session (rotated ~15 min) | Engine, on every browser request | Minted 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
createUnlinkClientfrom@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
createUnlinkClientfrom@unlink-xyz/sdk/clientwith 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 asignSigningRequestcallback so your backend never holds the spending key.
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.