> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unlink.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Trust model

> Where each secret lives and which process it crosses.

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`](https://github.com/unlink-xyz/monorepo/tree/main/protocol/sdk).

## Data flow

```mermaid theme={null}
flowchart LR
    subgraph Client["Client process (browser, CLI, agent)"]
        K1["Spending key (never leaves)"]
        K2["Viewing + nullifying keys (sent at registration)"]
        S1["SDK signs SigningRequest locally"]
    end

    subgraph Engine["Unlink Engine"]
        E1["Groth16 proof generation"]
        E2["Transaction assembly"]
    end

    subgraph Relayer["Relayer"]
        R1["Gas sponsorship"]
        R2["EVM broadcast"]
    end

    subgraph Chain["Public blockchain"]
        C1["Unlink contract verifies proof"]
    end

    K1 -->|"signed request"| S1
    S1 -->|"signature + plan"| E1
    K2 -.->|"once, at registration"| E1
    E1 --> E2
    E2 -->|"calldata"| R1
    R1 --> R2
    R2 --> C1

    style Client fill:#ffffff,stroke:#1a1a1a,color:#1a1a1a
    style Engine fill:#ffffff,stroke:#b8b6b1,color:#1a1a1a
    style Relayer fill:#ffffff,stroke:#b8b6b1,color:#1a1a1a
    style Chain fill:#ffffff,stroke:#b8b6b1,color:#1a1a1a
```

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 [`createUnlinkClient` from `@unlink-xyz/sdk/browser`](/quickstart#browser-non-custodial-client). 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](/custody-models) for the full comparison.

<Note>
  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.
</Note>
