centaur wallet

A wallet whose private key is never assembled — not on our servers, and not in your browser

Centaur Wallet is a non-custodial wallet built on threshold signatures. A conventional wallet has one private key: whoever holds it can spend, and every design decision after that is an attempt to keep one secret in one place safe. This one has no such key. At setup the wallet generates three shares — one in your browser, one held by a coordination service, one kept in cold storage for recovery — and a signature is produced by two of them working together, using FROST, the threshold Schnorr scheme standardised as RFC 9591. The shares are never combined, not even for an instant, and no participant ever learns another's.

What that buys is concrete: stealing the share in your browser is not enough to move funds, and neither is compromising the coordinator. There is no seed phrase to write down and no single file whose theft is terminal. What it costs is that signing becomes a protocol between parties rather than a local computation — which is what the demo exists to show you, round by round, as it happens.

The signing itself runs as a Rust implementation of FROST compiled to WebAssembly, inside a browser tab. This site is a demonstration stand for a system under active development. It is not a product you can put money into, it holds no real funds, and nothing it signs is broadcast to any network.

Open the demo Sign in with an email address — no funds, no accounts worth having.

How it works

1. The key is generated in pieces

Distributed key generation (DKG) runs as four rounds between the three participants. Each ends up with a share of a key that was never whole anywhere. What comes out of it publicly is a group verifying key — the wallet's Ed25519 address, and the only part of the key material that is meant to be seen.

2. Two parties sign, jointly

A transaction is signed by two shares in a two-round protocol: commitments first, then signature shares that are aggregated into one ordinary Ed25519 signature. The chain sees a single signature from a single address and cannot tell it was produced by more than one party — the threshold structure costs nothing on-chain and is invisible to the network.

3. A policy sits in the middle

Because a second party must agree before anything is signed, that party can hold a transfer to the rules you set. A transaction over your limit is queued with a delay and an email that can cancel it — from a device other than the one that asked. A stolen session on your laptop does not survive a message to your inbox.

Why the signing panel is a separate frame

The dark panel beside the demo is not part of the demo page. It is an iframe served from a different origin, and the browser's same-origin policy is what separates them: script running in the page cannot read its memory, its storage, or the share it holds, and the only thing that crosses between them is a structured message with a fixed set of shapes. The page asks for a signature; it never asks for the share, because there is no message that would answer.

That boundary is the reason an application can embed this wallet without being trusted with anything. It is also why the two halves are built differently on purpose: the pages you are reading are an ordinary web application with a framework and a design system, and the signing frame is a deliberately small program with a strict Content Security Policy and no dependency that is not needed to hold a key share. Everything added here is surface area that cannot reach it.

The same reasoning is why this page and the demo are two pages rather than one. What the demo needs beside it is a sentence and a signing frame; what an explanation needs is room. Splitting them means the page that asks for your email address is not also the page carrying every paragraph on this site, and the page you are reading now runs no script at all.

How your share is kept, and how it is lost

The share generated in your browser is the one you hold, and it stays on the signing frame's own origin — never on the page that embeds it. It is written to storage sealed under a key you choose how to derive, and it is unsealed into the frame's worker only for as long as it is being used. A page reload ends that: the wrapped share survives, the copy in memory does not, and the next signature asks you to unlock again. Unlock the same wallet after a reload and the same address comes back, because the share is the same one.

Three ways to unlock it, chosen per wallet and changeable at any time:

  • This device. The share is sealed under a key this browser holds and cannot export. Nothing to type and nothing to remember — and nothing to recover it with, so a browser profile that is cleared takes the wallet with it.
  • Passphrase. Asked for inside the signing frame, never seen by the embedding page, and stretched with Argon2id before it is used to seal anything. The unlocked copy locks itself again after five idle minutes.
  • Passkey. Unlocks with the same gesture as a passkey login, using the authenticator's PRF extension, where the browser and the authenticator support it. Where they do not, the demo says so rather than falling back.

Switching between them re-seals the share that is already there; it never runs a new key generation, and it never falls back to a weaker mode when the one you asked for fails. A wallet that cannot be sealed the way you asked stays sealed the way it was.

What none of that survives is the share simply not being there. In this version the third share is generated during key creation and discarded — the backup vault is a separate service and is not wired in — so a browser holding no share for a wallet cannot sign for it, and no passphrase or passkey changes that. The demo says so on the wallet in question rather than when you try to use it. Storage is also partitioned by the site doing the embedding, which the browser decides and the frame cannot see: a share sealed through one site's embed of this signer is not reachable through another's.

What the demo does

Sign in with an email address to get a one-time code, create a wallet and watch the four DKG rounds go by, then sign a test transfer and read back what the coordinator understood the transaction to be — amounts and destinations, not a hex blob. Then send one over the limit and watch it get held, with the cancellation link arriving out of band. Nothing there is broadcast, and the balances are fictional.

Open the demo →

What this demo proves, and what it does not

It does show

  • A key generated as shares, and an address derived from a key nobody ever held.
  • A signature produced across an origin boundary, with the share never leaving its own origin.
  • A transaction parsed and judged by the second party before it agrees to sign.
  • A held transfer, its countdown, and a cancellation that arrives by email rather than by browser.
  • A share that survives a full page reload, and the unlock step that brings it back.

It does not show

  • Real funds. No transaction here is broadcast to any network, and no balance is real.
  • An audited system. The cryptography is under review and the deployment is a stand, not a service.
  • Protection against a compromised browser. A page that is already running an attacker's code can ask for signatures the same way the demo does — which is what the policy layer and the out-of-band cancellation exist to bound.
  • Recovery. The third share and the backup path are a separate piece of work.