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

# Unlinkability — two apps, one engine

> The same credential produces a different nullifier in every app. Reusable credentials, without a reusable tracking identifier.

A reusable credential is convenient — and dangerous. If the same credential produced the same on-chain marker everywhere, it would become a tracking beacon: anyone watching the chain could link a user's activity across every app that accepts it.

Nullis keeps credentials reusable **without** making them trackable, by **domain-separating** the nullifier.

## The threat, concretely

```mermaid theme={null}
flowchart TB
    subgraph BAD["❌ Without domain separation"]
        S1["credential_secret"] --> X["nullifier = f(secret)"]
        X --> XA["seen in App A"]
        X --> XB["seen in App B"]
        XA -.->|"same value → LINKED"| XB
    end
    style BAD fill:#0a0a0c,stroke:#FF3B30,color:#fff
    style S1 fill:#16161a,stroke:#FF3B30,color:#fff
    style X fill:#16161a,stroke:#FF3B30,color:#fff
    style XA fill:#16161a,stroke:#FF3B30,color:#fff
    style XB fill:#16161a,stroke:#FF3B30,color:#fff
```

If the nullifier were just a function of the secret, the *same* value would surface in every app — a permanent, cross-app identifier. That's the exact tracking harm private credentials are supposed to avoid.

## The mechanism

The nullifier folds in an `app_domain` term:

```txt theme={null}
nullifier = Poseidon(credential_secret, policy_id, app_domain, action_id)
```

Because Poseidon is a cryptographic hash, changing any input scrambles the output unpredictably. Change the app, change the `app_domain`, and the nullifier is a completely different, uncorrelatable field element — even for the identical credential. Given only two nullifiers, an observer cannot tell whether they came from one credential or two.

```mermaid theme={null}
flowchart TB
    SEC["One credential_secret"]
    SEC --> A["App A (remittance)<br/>app_domain = 44240"]
    SEC --> B["App B (RWA access)<br/>app_domain = 48879"]
    A --> NA["nullifier_A<br/>0x09e0…a08c"]
    B --> NB["nullifier_B<br/>0x0532…2911"]
    NA -.->|"unlinkable"| NB
    style SEC fill:#16161a,stroke:#FF3B30,color:#fff
    style A fill:#16161a,stroke:#57B87F,color:#fff
    style B fill:#16161a,stroke:#57B87F,color:#fff
    style NA fill:#16161a,stroke:#00E676,color:#fff
    style NB fill:#16161a,stroke:#00E676,color:#fff
```

<Info>
  The `app_domain` also does double duty for **replay protection**: because it's folded into the nullifier alongside `action_id`, each app maintains its own independent spent-set. A nullifier spent in App A has no bearing on App B.
</Info>

## Proven, not asserted

This is a real, reproducible result — the same credential used in the remittance app and the RWA-access app yields two different nullifiers:

```bash theme={null}
npm run build -w @nullis/core -w @nullis/sdk -w @nullis/issuer
node examples/unlinkability.mjs
```

| App        | Policy | app\_domain | Nullifier             |
| ---------- | ------ | ----------- | --------------------- |
| Remittance | 777    | 44240       | `0x09e01e5e…436ba08c` |
| RWA access | 888    | 48879       | `0x0532e4a7…357f2911` |

Same `credentialCommitment`, two unrelated nullifiers. Two apps, one engine, cryptographically unlinkable at the proof and nullifier layer.

## The scope of the claim

<Warning>
  Unlinkability is scoped **precisely**. The same credential yields different nullifiers per app, unlinkable *at the proof and nullifier layer*. It does **not** hide:

  * **Wallet addresses** — the submitting account is public on both apps.
  * **Funding sources** — shared funding can correlate accounts.
  * **IP and timing** — network-level metadata lives outside the artifact layer.

  Nullis never overclaims beyond the artifact layer.
</Warning>

This precision is itself a feature: Nullis tells you exactly where its privacy guarantee ends, so you can reason about the rest of your stack — wallets, RPC, funding — honestly, instead of assuming a blanket "anonymous."

<Card title="Next: revocation" icon="rotate" href="/crypto/revocation">
  How access is revoked without touching anyone's identity.
</Card>
