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

# User authentication and viewing keys

> Wallet-signature-gated access to recipient keys held inside the secure enclave. No client-side key storage required.

<Warning>
  **The TypeScript SDK is WIP.** Today, call the [Relayer HTTP API](/api/relayer) directly: `POST /recipient/register`, `GET /recipient/addresses`, `POST /recipient/derive-stealth-key`. The shapes below mirror those endpoints.
</Warning>

Tachyon doesn't require usernames, passwords, or accounts. A user is identified by their **wallet**.

## The key model

The recipient's viewing and spending keys are generated and held inside Tachyon's secure enclave on `POST /recipient/register`. From then on, any action that needs those keys (listing stealth balances, deriving a stealth key to sweep funds, producing a disclosure record) is performed by the enclave after verifying a wallet signature from the user.

**Integrators don't store viewing keys client-side.** The enclave is the source of truth; the wallet signature is the authorization.

| Item                    | Who generates              | Where it lives                                 | How you access it                        |
| ----------------------- | -------------------------- | ---------------------------------------------- | ---------------------------------------- |
| Wallet (signing key)    | User's wallet              | User's wallet. You don't touch the private key | Request a signature                      |
| Viewing + spending keys | Secure enclave on register | Inside the enclave                             | Call the relayer with a wallet signature |

This design means a lost device doesn't lose the keys. A new device onboards by re-proving wallet ownership to the relayer.

## Register a user

```ts theme={null}
const message = `REGISTER_STEALTH:${userAddress}`;
const signature = await userWallet.signMessage(message);

const res = await fetch("https://relayer.tachyon.pe/recipient/register", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ recipientAddress: userAddress, signature }),
});
```

Idempotent: calling again for the same address returns existing public keys, it does not re-generate.

## Read operations (all wallet-gated)

### List the user's stealth addresses

```ts theme={null}
const message = `REGISTER_STEALTH:${userAddress}`; // same canonical register message
const signature = await userWallet.signMessage(message);

const res = await fetch(
  `https://relayer.tachyon.pe/recipient/addresses?address=${userAddress}&signature=${signature}`,
);
const { stealthAddresses } = await res.json();
```

### Derive a stealth key to sweep a specific delivery

```ts theme={null}
const message = `DERIVE_STEALTH_KEY:${userAddress}:${stealthAddress}`;
const signature = await userWallet.signMessage(message);

const res = await fetch("https://relayer.tachyon.pe/recipient/derive-stealth-key", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ recipientAddress: userAddress, stealthAddress, signature }),
});
const { stealthPrivateKey } = await res.json();
```

The derived key lives only as long as your session needs it. Use it to sign a Safe transaction (or hand it to the relay-proxy sweep flow, which is gasless for the user).

## What about client-side persistence?

Don't, unless you have a specific reason. The common instinct is to cache a viewing key in the browser so the user "stays logged in". We'd rather you didn't:

* **Browsers leak.** Any browser-side storage (localStorage, IndexedDB, even session storage) is within reach of XSS, extensions, or shared-device scenarios. The blast radius of a leaked viewing key is "everything this user ever receives".
* **The enclave is always available.** Re-fetching on demand with a fresh wallet signature is a single round trip.
* **The wallet is already the user's identity.** Asking them to sign once per session is a familiar UX pattern.

If you absolutely need to cache (e.g. a native mobile app with a long-lived session):

1. Keep the derived stealth key, not the viewing key. Derived stealth keys are per-intent; one leaking compromises one delivery, not all future inflows.
2. Encrypt with a key the user sets (PIN, biometric-unlock, wallet-signed KDF). Never in cleartext.
3. On iOS/Android, use Keychain/Keystore; never raw app storage.

## Backup and recovery

The enclave-resident keys are recoverable by re-registering with the same wallet signature. "Losing" a viewing key client-side is not a problem because there is no client-side source of truth.

The user loses access **only if they lose control of the wallet** that registered the address. That's the same failure mode as any other on-chain account, and your existing wallet-recovery UX covers it.

<Note>
  There is no separate recovery phrase for viewing keys. Wallet ownership is the recovery mechanism.
</Note>

## Multiple wallets per user

If a user wants to treat two wallets as separate identities (say, one per workspace), each wallet registers independently and the relayer stores distinct key material for each. Switching is a session-level signing operation.

## What integrators do *not* manage

* Encryption of intent payloads (enclave handles it)
* Stealth address derivation (enclave handles it using the recipient's enclave-held keys)
* Sweep transactions from the stealth address (gasless via `POST /recipient/relay-proxy`)

You stick to wallet flows; the relayer and enclave handle the rest.

<Card title="Submit an intent" icon="paper-plane" href="/sdk/submitting-intents">
  Build, sign, and submit your first intent.
</Card>
