···991010- ✅ **IPFS storage** — repo blocks and blobs are stored on a local Kubo node and indexed in SQLite by DID and CID.
1111- ✅ **Keyless PDS** — the server never stores a private key. Every write is signed by the user's secp256k1 key in their Ethereum wallet (Rabby, MetaMask, etc.).
1212-- ✅ **Browser signer** — the account page connects over WebSocket and signs commits with the user's Ethereum wallet. No browser extension is needed; just keep the tab open. Standard ATProto clients do not need to know about it.
1212+- ✅ **Browser signer** — the account page connects over WebSocket and signs repo commits and PLC operations with the user's Ethereum wallet. No browser extension is needed; just keep the tab open. Standard ATProto clients do not need to know about it.
1313- ✅ **User-controlled DID** — when the user registers a key, the PDS transfers the `did:plc` rotation key to the user's wallet. After that, only the user can change their identity.
1414- 🔜 **x402 payments for IPFS storage** — blob uploads will be gated by on-chain payments via the [x402 protocol](https://x402.org), using the same Ethereum wallet.
1515···146146147147### BYOK (Bring Your Own Key)
148148149149-The PDS **never stores or uses a private key**. Every write from the Bluesky app, Tangled, or any other standard ATProto client is held open until the user's Ethereum wallet returns a signature through the browser signer on the account page.
149149+The PDS holds one key — the **rotation key** — used only for lightweight PDS-level operations (service-auth JWTs, genesis DID creation, and PLC operations before the user has registered their wallet). It is never used to sign user content.
150150+151151+Every repo write from the Bluesky app, Tangled, or any other standard ATProto client is held open until the user's Ethereum wallet returns a signature through the browser signer on the account page.
150152151153#### End-to-end signing flow
152154···169171170172**What requires a wallet signature:**
171173172172-Only operations that change the user's repo or identity need a wallet signature:
174174+Only operations that change the user's repo content, or identity operations after the user has taken ownership of their rotation key, need a wallet signature:
173175174176- **Repo writes** — `createRecord`, `putRecord`, `deleteRecord`, `applyWrites`
175175-- **Identity operations** — PLC operations, handle updates
177177+- **Identity operations** — PLC operations and handle updates, **once the user's wallet key is the rotation key**. Before that, the PDS rotation key signs them directly.
176178- **x402 payments** — EIP-712 payment authorisations for gated pinning
177179178178-Read-only operations (browsing feeds, loading profiles, fetching notifications, etc.) do **not** prompt the wallet. The PDS proxies them to the AppView using cached service-auth JWTs — see [Service auth caching](#service-auth-caching) below.
179179-180180-#### Service auth caching
181181-182182-In standard ATProto, the PDS signs service-auth JWTs itself because it holds the signing key. In Vow, the signing key is in the user's wallet, so without caching, every proxied request (feeds, profiles, notifications, and so on) would trigger a wallet prompt.
183183-184184-Vow solves this by **caching service-auth JWTs**. When the proxy needs a token for an `(aud, lxm)` pair, it checks an in-memory cache first. Only a cache miss triggers a wallet signing request. Cached tokens live for 30 minutes with a 15-second reuse margin, so in practice the wallet is prompted at most **once every ~30 minutes per remote service endpoint** instead of on every request.
185185-186186-Tokens requested explicitly via `com.atproto.server.getServiceAuth` (where the caller controls the expiry) bypass the cache and always go to the wallet.
180180+Read-only operations (browsing feeds, loading profiles, fetching notifications, etc.) do **not** prompt the wallet. Service-auth JWTs for proxied requests are signed directly by the PDS rotation key — fast, in-process, no wallet involved.
187181188182#### WebSocket connection
189183···345339- [x] `com.atproto.server.requestEmailConfirmation`
346340- [x] `com.atproto.server.requestEmailUpdate`
347341- [x] `com.atproto.server.requestPasswordReset`
348348-- [x] `com.atproto.server.reserveSigningKey`
349342- [x] `com.atproto.server.resetPassword`
350343- [x] `com.atproto.server.updateEmail`
351344