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

# The non-custodial model (Confiroll-blind)

> Two secrets per user, browser-side proving, Fork B fee-bump, employer-held escrow, and the invariant that Confiroll holds zero user keys.

This is the heart of Confiroll. The design goal is a single invariant:

<Info>
  **Confiroll holds zero user secrets.** Amounts are hidden from the public, from other
  employers, **and from Confiroll itself**. This is what *Confiroll-blind* means.
</Info>

Everything else (the two keys, browser-side proving, the Fork B fee-bump, the employer-held
escrow) exists to make that invariant true while still being usable with real wallets and
gas-free for you.

## Two secrets per user

Every actor, employer and contractor alike, holds **two independent secrets**:

<CardGroup cols={2}>
  <Card title="Stellar ed25519 key" icon="signature">
    A standard Stellar keypair (`G...` address). It sources and signs the on-chain transaction.
    This is what SEP-10 authenticates and what the fee-bump wraps. Lives in the wallet.
  </Card>

  <Card title="Confidential scalar sk" icon="eye">
    A field element unrelated to the Stellar key. It authorizes movement of the *hidden*
    balance inside the zero-knowledge proof, and decrypts your own amounts. Lives on the
    device.
  </Card>
</CardGroup>

### Why two keys

The two secrets do two different jobs, and neither can do the other's:

* The **Stellar ed25519 key** authorizes *transactions*. It sources the on-chain call, signs
  the envelope, and satisfies `require_auth`. It is what SEP-10 challenges and what the fee-bump
  wraps. It says nothing about amounts.
* The **confidential scalar `sk`** authorizes and reads *value*. It proves you own the hidden
  balance you are spending, and it decrypts amounts encrypted to you. It never signs a Stellar
  transaction.

Splitting these means the on-chain identity that the network sees is fully separated from the
value layer. The chain and any observer see a signed transfer between two `G-addresses`. Only a
holder of the right viewing key can read the amount. One key authorizes, the other key decrypts
and proves, and they are cryptographically independent.

### Derivation

From `sk` and the token's address, the client derives everything else. The Stellar key is
never involved in the confidential math:

| Value    | Derivation                           | Role                                                  |
| -------- | ------------------------------------ | ----------------------------------------------------- |
| `sk`     | the one secret (a scalar)            | Spend authority + decryption                          |
| `vk`     | `Poseidon2(VIEWING_KEY, sk, addr_f)` | Contract-bound viewing key                            |
| `Y`      | `sk * H`                             | Public spending key                                   |
| `PVK`    | `vk * H`                             | **Public** viewing key that others encrypt payouts to |
| `addr_f` | `address_to_field(tokenAddress)`     | Binds a key set to one token deployment               |

`H` is a Grumpkin generator with no known discrete-log relationship to Stellar's base point,
so the two key systems are cryptographically independent. Because `vk` folds in `addr_f`, a
key set derived for one confidential-token deployment is meaningless against another.

<Note>
  In the custody model, `sk` is derived on your device as a KDF over a wallet signature on a
  fixed Confiroll-domain message, so your confidential key is deterministic from your wallet and
  never has to be stored or uploaded. The testnet client scripts also generate `sk` locally, as
  a fresh random scalar per run. Either way, `sk` never reaches a Confiroll server.
</Note>

## How sk is derived and used, on-device

The full life of the confidential key stays on your machine. Nothing in this sequence sends a
secret to a server:

<Steps>
  <Step title="Sign a fixed domain message">
    Your wallet signs a fixed Confiroll-domain message with your Stellar ed25519 key. This is a
    plain `signTransaction`-style signature, and the message is constant, so the same wallet
    always produces the same signature.
  </Step>

  <Step title="Derive sk from the signature">
    The client runs a KDF over that signature to produce the scalar `sk`. Because the input is
    deterministic, `sk` is deterministic: you can re-derive it on any device with the same
    wallet, and you never have to store or back it up separately.
  </Step>

  <Step title="Derive the rest locally">
    From `sk` and `addr_f` the client computes `vk`, `Y`, and `PVK` with the derivations in the
    table above. `Y` and `PVK` are public; `sk` and `vk` stay on the device.
  </Step>

  <Step title="Prove and decrypt with sk, never send it">
    `sk` and `vk` feed the bb.js prover (for transfer and withdraw) and the decryptor (to read
    your own balances). Only public outputs leave the browser: a signed transaction, a proof,
    and public keys. The secret itself never travels.
  </Step>
</Steps>

<Warning>
  Because `sk` derives from a wallet signature, it inherits the wallet's security. Keep the
  wallet safe. There is no server-side copy of `sk` to fall back on, which is the point:
  Confiroll cannot recover it, and neither can an attacker who breaches Confiroll.
</Warning>

## Why privacy holds

Two independent facts combine:

1. **Amounts are encrypted on-chain.** A `confidential_transfer` carries **no plaintext
   amount argument**. The value lives only inside the proof and encrypted event fields.
   Reading an amount requires a viewing key.
2. **Confiroll never has a viewing key.** `sk` (and therefore `vk`) is derived and used only
   on your device. The server receives a *signed transaction*, never a secret.

So the only parties who can read a payout's amount are the payer, the payee, and the holder
of the escrow key, never Confiroll, and never the public.

<Warning>
  Privacy here is **confidentiality, not anonymity**. The transaction graph (who paid whom,
  and when) is public on-chain. Confiroll hides *amounts*, not the existence or the parties of
  a payment. Confiroll never claims payments are "unlinkable."
</Warning>

## What Confiroll can and cannot do

The invariant becomes concrete when you list it as capabilities. Confiroll's reach stops at
public data and fee sponsorship:

| Action                            | Can Confiroll do it? | Why                                                                                                   |
| --------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------- |
| Move your funds                   | No                   | It never holds your Stellar signing key; it cannot source or sign a transfer for you.                 |
| Read a payout amount              | No                   | Amounts are encrypted to viewing keys; Confiroll holds no viewing key.                                |
| Recover your keys                 | No                   | `sk` derives on your device from a wallet signature; there is no server-side copy.                    |
| Read your ongoing balance         | No                   | Balances are commitments; decrypting them needs `sk`, which Confiroll never sees.                     |
| Fee-bump a transaction you signed | Yes                  | The Fork B sponsor wraps your signed inner transaction and pays the outer fee.                        |
| Serve public directory data       | Yes                  | It can list a recipient's address and public viewing key `PVK`, which are public by design.           |
| Refuse to sponsor                 | Yes                  | The sponsor enforces an allow-list, a fee cap, and a per-account quota, and returns `422` on refusal. |

The pattern: Confiroll can help you pay (by sponsoring the fee) and can help others pay you (by
publishing your public keys), but it can never spend, read, or recover.

## Fork B: the pivotal authorization decision

The confidential token's `confidential_transfer(from, to, data)` calls `from.require_auth()`.
*How* that auth is satisfied depends on **who is the transaction source**, and that choice
decides whether the whole thing is usable and non-custodial.

<Tabs>
  <Tab title="Fork A: channel-account source (rejected)">
    If a Confiroll/SDP channel account is the tx source, `require_auth(from)` surfaces as a
    **Soroban auth entry** that the employer must sign with `signAuthEntry`. That path is
    broken in Freighter and unimplemented in a large share of wallets. It only works with a
    custodial in-process key or a smart-contract wallet. **Rejected.**
  </Tab>

  <Tab title="Fork B: employer-as-source + fee-bump (chosen)">
    The employer is the **tx source**, so `require_auth(from)` is satisfied by ordinary
    **source-account auth**: a plain transaction-envelope signature via `signTransaction`,
    which every wallet supports. A CAP-15 **fee-bump** then wraps the signed inner transaction
    so the employer pays **0 XLM**. The sponsor never needs the employer's secret. It only
    pays the outer fee. **Chosen.**
  </Tab>
</Tabs>

Fork B is the only path that is simultaneously **non-custodial**, **0-XLM**, and **works with
real wallets**. It's why the core endpoint is `POST /transfer` (fee-bump a browser-signed
transfer), not a custodial "send payment" call. See
[Fee sponsorship](/developers/fee-sponsorship) for the sponsor and its guard-rails.

## Where every secret lives

| Secret                              | Held by         | Storage                                             |
| ----------------------------------- | --------------- | --------------------------------------------------- |
| Employer Stellar key                | **Employer**    | Wallet / embedded wallet, never Confiroll           |
| Employer confidential `sk`          | **Employer**    | Browser (KDF from wallet signature), never uploaded |
| Contractor Stellar key              | **Contractor**  | Own wallet / passkey, never Confiroll               |
| Contractor confidential `sk`        | **Contractor**  | Browser, never uploaded                             |
| Escrow / disclosure key             | **Employer**    | Employer-held, client-side, never Confiroll         |
| Fee-bump sponsor key                | Confiroll       | Sealed env + allow-list + fee cap + quota           |
| USDC issuer (faucet)                | Confiroll       | Sealed env                                          |
| SDP distribution + channel accounts | Confiroll (SDP) | SDP-managed                                         |

Confiroll's only keys are **operational**: the fee-bump sponsor, the testnet USDC faucet
issuer, and SDP's channel/distribution accounts. None of them can read an amount or spend your
balance.

## Consequences

Because proving, `sk` derivation, and signing are all client-side, the confidential
heavy-lifting lives in the **browser**, and `payroll-api` stays **thin**:

* **Client-side:** wallet signature to `sk`; zero-knowledge proving (bb.js WASM) for transfer
  and withdraw; contractor self-registration; employer-held escrow decryption for
  disclosure.
* **Server-side:** session auth; the Fork B fee-bump submit; a directory of **public** data
  (recipient address + public viewing key); the testnet faucet.

Escrow-key custody is what keeps Confiroll blind: because the *employer* holds the disclosure
key for their own payroll, compliance disclosures work without Confiroll ever being able to
read a payout.

## FAQ

<AccordionGroup>
  <Accordion title="If I lose my wallet, can Confiroll restore my confidential key?">
    No. `sk` derives on your device from a wallet signature, and there is no server-side copy.
    Restore the wallet and you can re-derive `sk` deterministically from the same signature.
    Lose the wallet with no backup, and neither you nor Confiroll can recover the key. That is
    the direct cost of Confiroll holding zero user secrets.
  </Accordion>

  <Accordion title="Why not let Confiroll hold sk to make onboarding easier?">
    Holding `sk` would let Confiroll decrypt every amount, which breaks the Confiroll-blind
    invariant. The whole architecture exists so that no single Confiroll breach can expose
    payouts. Onboarding friction (a contractor must register a wallet and self-register before a
    first payout) is the accepted trade for that guarantee.
  </Accordion>

  <Accordion title="Does the Stellar key ever touch the confidential math?">
    No. The Stellar ed25519 key sources and signs transactions and satisfies `require_auth`. The
    confidential math runs over Grumpkin with the scalar `sk`, and `H` has no known discrete-log
    relationship to Stellar's base point. The only crossover is that a wallet signature seeds the
    KDF that produces `sk`; after that, the two key systems are independent.
  </Accordion>
</AccordionGroup>
