Register end users, create delegated wallets, and let users transact with their passkeys.
This guide covers the full implementation of delegated wallets: registering end users, creating wallets they control, and letting them perform actions with their passkeys.
We recommend creating wallets after registration rather than during it — this gives you more control over when and which networks to provision. Two approaches:
Your service account creates a wallet and delegates it to a user via the delegateTo field on Create Wallet. This is the simplest approach — the service account’s signer handles action signing automatically.
Copy
Ask AI
const wallet = await dfns.wallets.createWallet({ body: { network: 'EthereumSepolia', delegateTo: userId, // end user ID from registration },})
The end user can also create their own wallets using the delegated client. This follows the Init/Complete pattern — the user must sign with their passkey.
Wallets created by the end user are automatically delegated to them — no delegateTo field needed.
Policies do not apply to delegated wallets. By design, delegated wallets bypass the policy engine — the end user has full control without organizational approval requirements.
Once a user is logged in, they can perform actions on their wallets (transfers, signatures, etc.). These write operations require the user to sign with their passkey.
Use DfnsDelegatedApiClient (TypeScript) or DfnsDelegatedClient (Python) with the end user’s auth token. Unlike the service account client, this client does not have a signer — signing happens on the user’s device.
Copy
Ask AI
import { DfnsDelegatedApiClient } from '@dfns/sdk'const delegatedClient = new DfnsDelegatedApiClient({ baseUrl: process.env.DFNS_API_URL, authToken: endUserToken, // token from delegated login})
Every write method on the delegated client is split into two calls:
methodInit() — sends the request payload to Dfns, returns a challenge
methodComplete() — sends the same payload + the user’s signed challenge, executes the action
This split exists because the passkey lives on the user’s device, not on your server.
1
Init — get a challenge from Dfns
Copy
Ask AI
// Every write method has an Init variant: transferAssetInit, generateSignatureInit, etc.const challenge = await delegatedClient.wallets.transferAssetInit({ walletId, body: { kind: 'Native', to: '0xe5a2...', amount: '1000000' },})return { challenge } // send to frontend
2
Sign — user approves with their passkey (frontend)
Frontend
Copy
Ask AI
import { WebAuthnSigner } from '@dfns/sdk-browser'const webauthn = new WebAuthnSigner({ relyingParty: { id: 'your-domain.com', name: 'Your App' },})// Triggers passkey prompt (Touch ID, Face ID, etc.)const signedChallenge = await webauthn.sign(challenge)// Send signedChallenge back to your backend
3
Complete — execute the action
Copy
Ask AI
const transfer = await delegatedClient.wallets.transferAssetComplete( { walletId, body: { kind: 'Native', to: '0xe5a2...', amount: '1000000' } }, signedChallenge)
This pattern applies to all write operations: transferAssetInit/Complete, generateSignatureInit/Complete, createWalletInit/Complete, etc. See the signing flows reference for details including direct API calls without the SDK.