Skip to main content

Frictionless Wallets, Part 1: Device-Specific Key-Pairs

4 min read

This is part one of a three-part series on making on-chain wallets feel like normal apps. Part one is the foundation: a device-specific key-pair that a player's wallet delegates signing to, so an app can submit actions on their behalf without a pop-up every time. Part two adds social login with a 2-of-3 secret, and part three adds biometric unlock - both build on the key-pair described here.

This is a Cardano capability first: it works with the standard Cardano browser wallets (Lace, Eternl, Nami, NuFi), and the same code works across the other chains EffectStream supports.

See it in action​

The demo below runs the whole flow: a Cardano wallet (Lace) signs a message with a pop-up, the device key signs the same kind of message instantly, and then three production games run pop-up-free because the device key is signing every move behind the scenes.

Two kinds of transactions​

The idea rests on a distinction every game makes whether it says so or not:

  • Financial transactions - moving assets, minting, anything that spends value - always require explicit approval from the player's real wallet.
  • Non-financial transactions - game moves, messages, state updates - carry no value and just need to prove the player authored them.

Asking the player to approve the second kind in a wallet pop-up is what makes on-chain apps feel like filling in forms. The device key-pair removes exactly that friction, and only that.

Creating the key-pair in the browser​

When a session starts, the app generates a fresh key-pair on the device, in the browser, and keeps it in encrypted local storage. Nothing about it touches a server. The player then connects their real Cardano wallet once and signs a single delegation certificate that authorises the device key to submit non-financial transactions on their behalf.

That delegated key is deliberately limited:

  • Scoped - it can only submit non-financial inputs. It can never move funds; financial transactions still route through the real Cardano wallet.
  • Time-bounded - it expires with the session.
  • Revocable - the player can cancel the delegation at any time.

So the security guarantee players expect is preserved - nobody moves their assets without an explicit signature - while the per-action friction disappears.

The code​

The delegation lifecycle lives in @effectstream/wallets: key generation, certificate signing, session management, and silent signing of non-financial inputs. The flow is four steps:

import { WalletMode, walletLogin, signMessage } from "@effectstream/wallets";

// 1. Create the device-local key-pair, generated in the browser. With no
// `seedPhrase` a fresh BIP-39 seed is generated client-side; the app can
// persist it to encrypted local storage.
const device = await walletLogin({
mode: WalletMode.CardanoLocal,
network: "Preview", // "Mainnet" | "Preprod" | "Preview" | "Custom"
});

// 2. The player connects their real Cardano wallet (Lace, Eternl, Nami,
// NuFi...). This is the only step that shows a pop-up.
const real = await walletLogin({
mode: WalletMode.Cardano,
preference: { name: "lace" },
});

// 3. One-time delegation: link the device address to the player's account so
// the backend accepts the device key for non-financial inputs. The real
// wallet and the device key both sign the `&linkAddress` built-in - see
// `accountPayload.linkAddress` in @effectstream/concise.

// 4. Every later action is signed by the device key - no pop-up - then sent
// to the batcher.
const signature = await signMessage(device.result, "move|x10y20");

The signing model is uniform across chains: swap WalletMode.Cardano for WalletMode.Midnight, WalletMode.EvmInjected, or any other supported mode and the rest of the flow is identical. Full reference is in the wallets documentation.

Live today​

This is not a prototype - the device key-pair is in production on midnight.fun, powering three games where pop-ups per action would make play impossible:

What's next​

The device key-pair is the building block for the rest of this series:

Source​