Why
PR #136 shipped Approach A: Useroutr auto-provisions a managed Stellar wallet at signup, holds the seed encrypted under `SETTLEMENT_KEY_KEK`. This is the right onboarding model (zero friction, matches Stripe-style UX), but it's custodial-flavored.
The plan doc at `apps/api/docs/architecture/merchant-settlement-onboarding.md` proposes Approach B as the medium-term upgrade: a passkey-derived wallet where the merchant's WebAuthn credential signs Stellar transactions and Useroutr never sees the seed.
This is what unwinds the "we manage your keys" caveat and lets the marketing claim graduate from "managed by default, self-custody when you want" to "self-custody by default, managed as fallback."
Reading first
Scope
Three deliverables:
- Schema + service — extend `MerchantSettlementKey` to support `managed=false` rows with a `passkeyCredentialId` instead of an `encryptedSeed`
- Onboarding flow — UI on dashboard `/settings` to upgrade from managed → passkey
- Signing path — every place that currently decrypts the managed seed needs a fork that asks the passkey to sign instead
Design sketch (high-level)
Provisioning
- Merchant clicks "Upgrade to passkey wallet" in settings
- Browser prompts WebAuthn (Touch ID / Face ID / Windows Hello / YubiKey)
- Backend gets the public key + credential ID; uses Passkey Kit to deploy a Soroban smart wallet controlled by that credential
- Mark the new `MerchantSettlementKey` row with `managed=false`, `passkeyCredentialId`, `smartWalletAddress`
- (If migrating from a managed wallet) Move the USDC balance from the old managed account → new smart wallet, then delete the encrypted seed
- Old `Merchant.settlementAddress` mirror updates to the new smart-wallet address
Signing
Every place the current code calls `MerchantSettlementService.decryptSeed(row)` to sign needs a branch:
```ts
if (row.managed) {
// existing flow: decrypt + sign server-side
} else {
// new flow: build the tx, return XDR to the frontend,
// have the merchant sign with their passkey, return signed XDR
}
```
The only place this matters today is withdrawal (#147) — every other "settlement" interaction (receiving USDC from a CCTP V2 mint, viewing balance, etc.) doesn't need the seed at all. So passkey signing is gated on the withdrawal flow.
Withdrawal flow with passkey
- Merchant clicks "Withdraw" → fills the modal
- Frontend POSTs `/v1/merchants/me/settlement/withdraw/prepare` → backend builds the unsigned XDR
- Frontend prompts passkey + signs the XDR (using Passkey Kit's signing helper)
- Frontend POSTs `/v1/merchants/me/settlement/withdraw/submit { signedXdr }` → backend submits to Horizon
- Same response shape as the managed flow
Risks + mitigations
| Risk |
Mitigation |
| Merchant loses every device with the passkey → wallet is gone |
Passkey Kit's "secondary device" pattern. Add a `POST /v1/merchants/me/settlement/passkey/add-device` flow. |
| WebAuthn browser support varies (Safari iOS, older Android) |
Detect support before showing the upgrade CTA. Fall back to "stay on managed." |
| Soroban smart wallet has its own audit / deployment cost |
Passkey Kit handles deployment. Reserves on mainnet are still ~3 XLM (we sponsor). |
| Migration from managed → passkey loses funds if anything goes wrong |
Do the migration as a single atomic submit: payment op from managed → new wallet, signed by the still-existing managed seed. If it fails, both wallets exist; merchant can retry. Add a confirmation gate before the migration. |
Out of scope
- Recovery via social login / email (defer; rely on multi-device passkey first)
- Multi-sig / threshold wallets (defer; passkey + secondary device is enough for v1)
- Mainnet rollout (build on testnet first; flip mainnet per-merchant after smoke tests)
Acceptance criteria
Estimated effort
~3–5 days. The Passkey Kit integration is the unknown — could be smoother than expected if it ships good React/wagmi-like helpers, or rougher if we need to build wrappers.
Reading material for whoever picks this up
- Passkey Kit demo: https://passkey-kit-demo.fly.dev/
- Talk from Meridian 2024 on passkey wallets: search "Meridian passkey wallet stellar"
- The Useroutr settlement onboarding plan: `apps/api/docs/architecture/merchant-settlement-onboarding.md`
Why
PR #136 shipped Approach A: Useroutr auto-provisions a managed Stellar wallet at signup, holds the seed encrypted under `SETTLEMENT_KEY_KEK`. This is the right onboarding model (zero friction, matches Stripe-style UX), but it's custodial-flavored.
The plan doc at `apps/api/docs/architecture/merchant-settlement-onboarding.md` proposes Approach B as the medium-term upgrade: a passkey-derived wallet where the merchant's WebAuthn credential signs Stellar transactions and Useroutr never sees the seed.
This is what unwinds the "we manage your keys" caveat and lets the marketing claim graduate from "managed by default, self-custody when you want" to "self-custody by default, managed as fallback."
Reading first
Scope
Three deliverables:
Design sketch (high-level)
Provisioning
Signing
Every place the current code calls `MerchantSettlementService.decryptSeed(row)` to sign needs a branch:
```ts
if (row.managed) {
// existing flow: decrypt + sign server-side
} else {
// new flow: build the tx, return XDR to the frontend,
// have the merchant sign with their passkey, return signed XDR
}
```
The only place this matters today is withdrawal (#147) — every other "settlement" interaction (receiving USDC from a CCTP V2 mint, viewing balance, etc.) doesn't need the seed at all. So passkey signing is gated on the withdrawal flow.
Withdrawal flow with passkey
Risks + mitigations
Out of scope
Acceptance criteria
Estimated effort
~3–5 days. The Passkey Kit integration is the unknown — could be smoother than expected if it ships good React/wagmi-like helpers, or rougher if we need to build wrappers.
Reading material for whoever picks this up