Existing wallets
Add passkeys to users who already hold a classic (G) Stellar account. The migration to a smart account, backup signers, and who pays fees on testnet vs mainnet.
SoroPass is not a wallet. It is the passkey-and-smart-account layer your wallet integrates: you keep your brand, your UI, your users, and your fee policy, and gain passkey accounts by wiring in the SDK. There is no "SoroPass" for end users to pick; the passkey option carries your wallet's own name and icon.
Wallet teams almost always ask the same thing: your users already have a classic account, so how do you add passkeys to it? The short answer shapes everything below.
The one rule
A passkey can only authorize a smart-account contract (C…), never a classic account (G…).
So "adding passkeys" always means a C contract exists behind them. The passkey is that
contract's signer. You do not upgrade a G account in place; you can't.
Why a passkey can't attach to a G-account
Two independent reasons:
- Wrong curve. A passkey signs with secp256r1 (P-256 / ES256). A classic account (
G…) only verifies Ed25519, plus pre-auth-tx and hashx signers. There is no P-256 (or contract) signer type for classic accounts, so a passkey signature is simply not something aGaccount can check. - Wrong signing shape. A WebAuthn assertion doesn't sign the raw transaction hash a classic account expects. It signs
authenticatorData ‖ SHA256(clientDataJSON), with the challenge bound insideclientDataJSON. Only a smart contract can reconstruct and verify that, which is what the smart wallet's__check_authdoes via the Soroban host functionsecp256r1_verify.
So the passkey controls a contract, and that contract is the wallet. See How signing works.
Migration: bring an existing G-account across
For your users who already hold funds in a G account, adding passkeys is a migration, not an in-place change:
Create the passkey → deploy a smart account
createPasskey mints the ES256 passkey and deploys its smart account. Your user now has a passkey-controlled C… address.
import { createPasskey } from '@soropass/core/create';
const account = await createPasskey({ ...cfg, userName: 'alice', deployer });
// account.contractId → the new C-address the passkey controlsMove value G → C
Build a normal payment (and any trustline moves) from the old G account to the new C account, and have your user sign it once with their existing classic key (Freighter, Ledger, a seed). This is plain classic Stellar (nothing passkey about it), so it isn't part of @soropass/core; your wallet already knows how to do it.
The passkey is now the wallet
From here every action is a passkey-signed smart-wallet transaction (sign). You can empty and abandon the old G account.
SoroPass owns the C side: create, sign, recover, add-signer. The one-time G → C transfer is
standard classic Stellar the wallet already performs; there is no SoroPass API for it because none
is needed.
No smart account yet?
Then there is nothing for the passkey to sign for: a passkey has no on-chain identity until its C contract is deployed. "Adding passkeys" is deploying that contract (step 1 above, via the deployer). Before it exists, the passkey is just a key on the device with nowhere to act.
Don't leave one device as a single point of failure
A wallet with exactly one passkey is lost if that device is lost. Add a second passkey as an additional signer so either device can approve on its own, using the add-device flow, backed by addSigner:
import { registerPasskey } from '@soropass/core/create';
import { addSigner } from '@soropass/core/recover';
const backup = await registerPasskey({ ...cfg, userName: 'backup' });
await addSigner({
walletContractId: account.contractId,
newSigner: backup,
...cfg,
sourceSecret,
sign,
});Adding a signer grants it full authority over the account, so gate it behind a fresh
re-authentication with an existing signer. See the recovery
model. (addSigner enrolls additional passkey signers; the native
account stores 65-byte secp256r1 keys.)
Who pays the fees
Authorization and payment are decoupled: the passkey says who can move funds; a classic account or a relayer pays the network fee and submits the transaction. Deploying the contract and every passkey-signed transaction needs a fee source.
| Network | Who pays |
|---|---|
| Testnet | A throwaway friendbot-funded classic keypair deploys the contract and pays. Free and disposable, and the passkey never touches it. |
| Mainnet | Either your user's own G account pays (fine if they already hold XLM), or a relayer / paymaster sponsors it so your user needs none. |
Gasless onboarding: sponsor fees with a relayer
For consumer wallets you want your user to sign with a passkey and pay no XLM up front. That is what the pluggable submission adapters are for: keep the default directSubmission (your source account pays) for development, and swap in a relayer for production.
import { sendSmartWalletTx, browserPasskeySigner } from '@soropass/core/sign';
import { launchtubeSubmission } from '@soropass/core';
await sendSmartWalletTx({
operation,
rpcUrl: cfg.rpcUrl,
networkPassphrase: cfg.networkPassphrase,
sourceSecret,
sign: browserPasskeySigner({ rpId: cfg.rpId, allowCredentials: [account.credentialId] }),
submission: launchtubeSubmission({
/* url, token */
}), // routes through the relayer, which sponsors the fee
});openzeppelinRelayerSubmission is the OZ Relayer equivalent. Every adapter returns the same SubmissionAdapter, so you move from the zero-infra default to a relayer without touching call sites. See Adapters.
Never ship a hot key in production. Mainnet wallets sponsor fees through a relayer (OpenZeppelin Relayer) or your user's own account, not a bundled secret.
For wallet teams
Offer passkeys as a new account type plus a migration path, not an edit to existing G accounts. Adopt SoroPass inside stellar-wallets-kit (the PasskeyModule, branded with your own productName and icon) or the headless @soropass/core, and pick your fee strategy (a relayer, or your user's account). The accounts are contracts; everything else (create, sign, recover, back up) is the same across every wallet that adopts the layer, and every user-facing surface stays your brand.
Guides
The full SoroPass flow on Stellar testnet. Create a passkey wallet, sign a payment, watch a wrong key get rejected on-chain, add a backup device, and recover on another device.
Fees and sponsorship
What a passkey account costs on-chain, who pays, and how to sponsor onboarding so users hold no XLM. Real mainnet numbers for account creation and add-device.