> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dfns.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Implement delegated wallets

> Register end users, create non-custodial delegated wallets, and let users sign their own transactions using passkeys with the DFNS API.

This guide covers the full implementation of [delegated wallets](/advanced/delegated-wallets): registering end users, creating wallets they control, and letting them perform actions with their passkeys.

<Tip>
  See a complete working example in the [nextjs-delegated SDK example](https://github.com/dfns/dfns-sdk-ts/tree/m/examples/sdk/nextjs-delegated).
</Tip>

## Setup: service account client

All backend operations use your [service account](/guides/developers/service-account). Set up the client once. It's used for registration, login, and wallet creation.

<CodeGroup>
  ```typescript Backend TypeScript theme={null}
  import { DfnsApiClient } from '@dfns/sdk'
  import { AsymmetricKeySigner } from '@dfns/sdk-keysigner'

  const signer = new AsymmetricKeySigner({
    credId: process.env.DFNS_SERVICE_ACCOUNT_CRED_ID,
    privateKey: process.env.DFNS_SERVICE_ACCOUNT_PRIVATE_KEY,
  })

  const dfns = new DfnsApiClient({
    baseUrl: process.env.DFNS_API_URL,
    authToken: process.env.DFNS_SERVICE_ACCOUNT_TOKEN,
    signer,
  })
  ```

  ```python Backend Python theme={null}
  import os
  from dfns_sdk import DfnsClient, DfnsClientConfig, KeySigner

  signer = KeySigner(
      credential_id=os.environ["DFNS_SERVICE_ACCOUNT_CRED_ID"],
      private_key=os.environ["DFNS_SERVICE_ACCOUNT_PRIVATE_KEY"],
      app_origin=os.environ.get("DFNS_APP_ORIGIN", "https://app.dfns.io"),
  )

  config = DfnsClientConfig(
      auth_token=os.environ["DFNS_SERVICE_ACCOUNT_TOKEN"],
      base_url=os.environ.get("DFNS_API_URL", "https://api.dfns.io"),
      signer=signer,
  )
  ```
</CodeGroup>

## Step 1: Register and log in end users

### Registration (new users)

Use the [Delegated Registration flow](/api-reference/auth/registration-flows#delegated-users-registration-flow). Your service account creates the user, your frontend collects the passkey.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant App as Your App
    participant DFNS

    User->>App: 1. Authenticate (your auth system)
    App->>DFNS: 2. Create user (service account)
    DFNS-->>App: 3. Registration challenge
    App->>User: 4. Create passkey
    User-->>App: 5. Passkey attestation
    App->>DFNS: 6. Complete registration
    DFNS-->>App: 7. User created
```

<Steps>
  <Step title="Create a registration challenge">
    Authenticate the user with your own system first, then call [Create Delegated Registration Challenge](/api-reference/auth/create-delegated-registration-challenge).

    <CodeGroup>
      ```typescript Backend TypeScript theme={null}
      const challenge = await dfns.auth.createDelegatedRegistrationChallenge({
        body: { email: user.email, kind: 'EndUser' },
      })
      ```

      ```python Backend Python theme={null}
      with DfnsClient(config) as dfns:
          challenge = dfns.auth.create_delegated_registration_challenge({
              "email": user["email"],
              "kind": "EndUser",
          })
      ```
    </CodeGroup>

    <Note>
      End users do not need a real email address. This field is the user's username (the registration challenge calls it `email`, delegated login calls it `username`, but it is the same value) and can be any unique string. We recommend **not** putting PII such as a real email address in it; use an opaque identifier instead. To correlate the user with your own system, set the optional `externalId` field.
    </Note>
  </Step>

  <Step title="User creates a passkey (frontend)">
    Send the challenge to your frontend. The browser prompts the user to create a passkey.

    ```typescript Frontend theme={null}
    import { WebAuthn } from '@dfns/sdk-browser'

    const webauthn = new WebAuthn({ rpId: 'your-domain.com' })
    const attestation = await webauthn.create(challenge)
    ```
  </Step>

  <Step title="Complete registration">
    Send the attestation back to your backend. Use [Complete User Registration](/api-reference/auth/complete-user-registration) with the `temporaryAuthenticationToken` from step 1 as the auth token.

    <Warning>
      **Register a recovery credential in the same call.** Bundle a `recoveryCredential` alongside `firstFactorCredential` so users who lose their device can regain access. Without one, a lost passkey means a permanently inaccessible wallet, and your only fallback is manual delegated recovery for every affected user. See [Implement end-user recovery](/guides/developers/end-user-recovery) for how to build the recovery credential.

      All credentials in the same `Complete User Registration` call sign the same challenge from step 1.
    </Warning>

    <CodeGroup>
      ```typescript Backend TypeScript theme={null}
      const dfnsWithTempToken = new DfnsApiClient({
        baseUrl: process.env.DFNS_API_URL,
        authToken: challenge.temporaryAuthenticationToken,
        signer,
      })

      const newUser = await dfnsWithTempToken.auth.register({
        body: {
          firstFactorCredential: attestation,
          recoveryCredential, // built on the frontend, see end-user-recovery guide
        },
      })

      // Store mapping: your user ID -> DFNS user ID
      await db.users.update({
        where: { id: yourUserId },
        data: { dfnsUserId: newUser.id },
      })
      ```

      ```python Backend Python theme={null}
      temp_config = DfnsClientConfig(
          auth_token=challenge["temporaryAuthenticationToken"],
          base_url=os.environ.get("DFNS_API_URL", "https://api.dfns.io"),
          signer=signer,
      )

      with DfnsClient(temp_config) as dfns_temp:
          new_user = dfns_temp.auth.complete_user_registration({
              "firstFactorCredential": attestation,
              "recoveryCredential": recovery_credential,  # built on the frontend
          })

      # Store mapping: your user ID -> DFNS user ID
      db.users.update(user_id=your_user_id, dfns_user_id=new_user["id"])
      ```
    </CodeGroup>
  </Step>
</Steps>

### Login (returning users)

Use [Delegated Login](/api-reference/auth/login-flows#delegated-users-login-flow). Authenticate the user with your system first, then get their DFNS auth token with a single call.

<CodeGroup>
  ```typescript Backend TypeScript theme={null}
  const { token } = await dfns.auth.delegatedLogin({
    body: { username: user.email },
  })
  ```

  ```python Backend Python theme={null}
  with DfnsClient(config) as dfns:
      result = dfns.auth.delegated_login({"username": user["email"]})
      token = result["token"]
  ```
</CodeGroup>

The returned `token` is the end user's auth token. You'll need it for [step 3](#step-3-delegated-actions).

## Step 2: Create delegated wallets

We recommend creating wallets after registration rather than during it. This gives you more control over when and which networks to provision. Two approaches:

### By your service account

Your service account creates a wallet and delegates it to a user via the `delegateTo` field on [Create Wallet](/api-reference/wallets/create-wallet). This is the simplest approach. The service account's signer handles action signing automatically.

<CodeGroup>
  ```typescript Backend TypeScript theme={null}
  const wallet = await dfns.wallets.createWallet({
    body: {
      network: 'EthereumSepolia',
      delegateTo: userId, // end user ID from registration
    },
  })
  ```

  ```python Backend Python theme={null}
  with DfnsClient(config) as dfns:
      wallet = dfns.wallets.create_wallet({
          "network": "EthereumSepolia",
          "delegateTo": user_id,  # end user ID from registration
      })
  ```
</CodeGroup>

### By the end user

The end user can also create their own wallets using the delegated client. This follows the [Init/Complete pattern](#the-init--complete-pattern). The user must sign with their passkey.

<CodeGroup>
  ```typescript Backend TypeScript theme={null}
  const challenge = await delegatedClient.wallets.createWalletInit({
    body: { network: 'EthereumSepolia' },
  })

  // ... user signs challenge with passkey on frontend ...

  const wallet = await delegatedClient.wallets.createWalletComplete(
    { body: { network: 'EthereumSepolia' } },
    signedChallenge
  )
  ```

  ```python Backend Python theme={null}
  challenge = delegated_client.wallets.create_wallet_init(
      body={"network": "EthereumSepolia"},
  )

  # ... user signs challenge with passkey on frontend ...

  wallet = delegated_client.wallets.create_wallet_complete(
      body={"network": "EthereumSepolia"},
      signed_challenge={
          "challengeIdentifier": challenge["challengeIdentifier"],
          "firstFactor": signed_challenge,
      },
  )
  ```
</CodeGroup>

Wallets created by the end user are automatically delegated to them. No `delegateTo` field needed.

<Warning>
  **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.
</Warning>

## Step 3: Delegated actions

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.

### The delegated client

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.

<CodeGroup>
  ```typescript Backend TypeScript theme={null}
  import { DfnsDelegatedApiClient } from '@dfns/sdk'

  const delegatedClient = new DfnsDelegatedApiClient({
    baseUrl: process.env.DFNS_API_URL,
    authToken: endUserToken, // token from delegated login
  })
  ```

  ```python Backend Python theme={null}
  from dfns_sdk import DfnsDelegatedClient, DfnsDelegatedClientConfig

  delegated_config = DfnsDelegatedClientConfig(
      auth_token=end_user_token,  # token from delegated login
      base_url="https://api.dfns.io",
  )

  delegated_client = DfnsDelegatedClient(delegated_config)
  ```
</CodeGroup>

### The Init / Complete pattern

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.

<Steps>
  <Step title="Init: get a challenge from DFNS">
    <CodeGroup>
      ```typescript Backend TypeScript theme={null}
      // 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
      ```

      ```python Backend Python theme={null}
      # Every write method has an _init variant: transfer_asset_init, generate_signature_init, etc.
      challenge = delegated_client.wallets.transfer_asset_init(
          wallet_id,
          body={"kind": "Native", "to": "0xe5a2...", "amount": "1000000"},
      )
      ```
    </CodeGroup>
  </Step>

  <Step title="Sign: user approves with their passkey (frontend)">
    ```typescript Frontend theme={null}
    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
    ```
  </Step>

  <Step title="Complete: execute the action">
    <CodeGroup>
      ```typescript Backend TypeScript theme={null}
      const transfer = await delegatedClient.wallets.transferAssetComplete(
        { walletId, body: { kind: 'Native', to: '0xe5a2...', amount: '1000000' } },
        signedChallenge
      )
      ```

      ```python Backend Python theme={null}
      transfer = delegated_client.wallets.transfer_asset_complete(
          wallet_id,
          body={"kind": "Native", "to": "0xe5a2...", "amount": "1000000"},
          signed_challenge={
              "challengeIdentifier": challenge["challengeIdentifier"],
              "firstFactor": signed_challenge,
          },
      )
      ```
    </CodeGroup>
  </Step>
</Steps>

This pattern applies to all write operations: `transferAssetInit/Complete`, `generateSignatureInit/Complete`, `createWalletInit/Complete`, etc. See the [signing flows reference](/api-reference/auth/signing-flows) for details including direct API calls without the SDK.

## Alternative: social registration

Instead of delegated registration, users can authenticate directly with an identity provider (like Google). No service account is needed.

<Tip>
  See the [auth-social SDK example](https://github.com/dfns/dfns-sdk-ts/tree/m/examples/sdk/auth-social) for a working implementation, and the [Social Registration flow](/api-reference/auth/registration-flows#social-registration-flow) / [Social Login API](/api-reference/auth/social-login) for the API reference.
</Tip>

## Security considerations

* **Never expose your service account credentials** to the frontend
* **Validate tokens** on your backend before creating DFNS users
* **Store the mapping** between your user IDs and DFNS user IDs securely
* **Implement rate limiting** on registration endpoints

## Recovery is not optional

Delegated wallets put signing authority on the user's device. If they lose that device and have no recovery credential, the wallet is permanently inaccessible. DFNS cannot restore it for them.

Before going to production, build the recovery flow:

* Register a `recoveryCredential` at signup, in the same `Complete User Registration` call as the first passkey (see the registration step above).
* Prompt users to add a second passkey on a different device shortly after onboarding.
* Generate a fresh recovery credential whenever a user completes recovery, since a successful recovery invalidates all previous credentials.

See [Implement end-user recovery](/guides/developers/end-user-recovery) for the full implementation.

## Next steps

<CardGroup cols={2}>
  <Card title="Configure WebAuthn" icon="key" href="/guides/developers/webauthn-configuration">
    Configure passkeys for your domain
  </Card>

  <Card title="Signing flows" icon="file-signature" href="/api-reference/auth/signing-flows">
    Full Init/Complete signing reference
  </Card>

  <Card title="Registration flows" icon="user-plus" href="/api-reference/auth/registration-flows">
    API reference for user registration
  </Card>
</CardGroup>
