Skip to main content

Embed a Privy wallet in an investor front end

Embed the Privy SDK in your investor front end, sign investors in with the token DALP issues, and send a gas-sponsored transfer from their wallet.

Overview

An investor can hold their own key in an embedded wallet from Privy, a third-party wallet provider. Your front end embeds the Privy SDK, signs the investor in to Privy with a token that DALP issues, and then calls the Platform API to link the wallet, create the identity, deploy the smart wallet and send transfers. DALP never holds the key: the investor's Privy wallet signs, and DALP prepares and submits what it signs.

The investor signs in to DALP, and Privy trusts the DALP token. Your front end needs no second login and no Privy account system of its own. Because Privy signs the investor in on that token, anyone who can sign in to DALP as the investor can also sign in to their Privy wallet, so protect the DALP sign-in as you would the wallet.

The embedded-wallet path applies to an investor who joins an organisation. Onboard an investor through the API covers managed custody, where the platform holds the key. Let an investor choose custody covers the choice between custody kinds and the browser-wallet path. Custody Provider describes both kinds.

Gas is sponsored when the organisation's gas payer is the paymaster. The smart wallet then needs no native currency, so an investor with an empty wallet can still transfer. When the organisation pays gas another way, the smart wallet pays its own gas and preparing a transfer from an empty wallet is refused with DALP-14034, which names the smart wallet and the shortfallWei it is missing. On and off ramps stay a Privy feature in your front end. The flow on this page has no fiat step.

The sequence at a glance

Seven steps from the offered provider to a sponsored transfer signed in Privy.Step 1 reads the offered providers and the Privy app id with GET /api/v2/custody/investor-provider. Step 2 signs the investor in to Privy with the token from GET /api/auth/token, which Privy checks against GET /api/auth/jwks. Step 3 creates the participant and proves the Privy key. Step 4 creates the identity. Step 5 deploys the smart wallet when the organisation uses account abstraction. Step 6 prepares the transfer with POST /api/v2/transaction-preparations. Step 7 signs the user operation typed data in Privy and finalises it, and the organisation's paymaster pays the gas.Seven steps from the offered provider to a sponsored transfer signed in Privy.The same figure as the desktop variant, drawn portrait for phone widths. The steps run in order: read the offered providers, sign in to Privy, create the participant and prove the key, create the identity, deploy the smart wallet, prepare the transfer, then sign in Privy and finalise.

Steps 1 and 2 happen before any wallet exists, and steps 3 to 5 repeat only until the investor is set up.

Embed the Privy SDK

Embed @privy-io/react-auth. The DALP Investor portal pins version 3.51.3, so match that version unless you have tested another. Wrap your application in PrivyProvider and set these options:

  • appId is the value DALP returns for the offered provider, and step 1 reads it. You do not choose it. A Privy configuration runs on the deployment's own Privy credentials or on the organisation's, so the Privy app behind appId, and its dashboard settings in Set up Privy to trust DALP, belong to the deployment operator or to the organisation.
  • clientId is set when DALP returns one.
  • externalWallets.disableAllExternalWallets is true, because the investor signs with the embedded wallet only.
  • embeddedWallets.ethereum.createOnLogin is "off". Create the wallet when the investor reaches that step, with the SDK's create wallet hook.
  • supportedChains and defaultChain name the organisation's network, which GET /api/v2/organizations/:organizationId/joinable-participants returns as chainId.

Set up Privy to trust DALP

The Privy dashboard needs custom authentication, which Privy calls JWT-based auth. The values are:

Privy setting
Value
JWT-based auth
On
User id claim
sub
JWKS endpoint
<platform URL>/api/auth/jwks
Allowed origin
The origin that serves your investor front end

<platform URL> is the public URL of your DALP deployment. Privy fetches the JWKS from that address, so the deployment must be reachable from the internet. The sub claim of the token is the investor's DALP user id.

Step 1: read the offered providers

Call GET /api/v2/custody/investor-provider. The response answers data, the organisation's designated investor provider or null, and options, every configuration the organisation offers to investors on the request's network, the default first. Each entry carries its id, its provider, its custody label and its clientConfiguration. Render options as they arrive: the platform has already dropped what the organisation does not offer and what does not serve the network, so a front end derives no label, default or network filter of its own. An entry for a Privy configuration carries clientConfiguration with kind set to privy, the tenant, the appId and, when set, the clientId. Take the Privy app id from there.

If no option offers Privy, do not show the Privy option. Which providers are offered is a setting, covered in Which provider is offered.

Step 2: sign the investor in to Privy

The investor is signed in to DALP already. Call GET /api/auth/token with that session, and DALP answers { "token": "<jwt>" }. Hand the token to Privy as the external JWT: with @privy-io/react-auth, pass a function that returns it to useSubscribeToJwtAuthWithFlag as getExternalJwt, together with isAuthenticated (true once the DALP session names a user) and isLoading (true while that session is still loading). Privy verifies the token against GET /api/auth/jwks and signs the investor in. Call the token route again each time Privy asks, because the token expires.

Create the embedded Ethereum wallet once Privy reports the investor as authenticated. The wallet address is the signing address you link in step 3.

Step 3: create the participant and prove the key

These calls run in this order, with the investor's DALP session:

  1. GET /api/v2/organizations/:organizationId/joinable-participants reads the organisation's network and whether it uses account abstraction (aaEnabled). It also lists participants the investor can reuse.
  2. POST /api/v2/participants with organizationId, the providerConfigurationId from step 1 and walletVerification creates a pending participant. Skip it when you resume an existing one.
  3. GET /api/v2/users/me/custody-accounts with participantId lists the participant's custody accounts. If one already links the Privy configuration on this network, reuse it and connect the same wallet.
  4. POST /api/v2/users/me/custody-account-control-preparations with providerConfigurationId, participantId and the wallet address as signingAddress returns a message, a binding and an account.
  5. The investor signs message with their Privy wallet as a personal message.
  6. POST /api/v2/users/me/custody-accounts/lifecycles with proof set to binding, account and an artifact returns a requestId. The artifact is the JSON string {"kind":"evm-self-custody-control","signature":"<signature>"}.
  7. GET /api/v2/users/me/custody-account-requests/:requestId with participantId reads the request. Follow it until its state is succeeded. The stream at the same path with /stream appended reports each change.

The account is ready when GET /api/v2/users/me/custody-accounts shows accountLifecycle and contextLifecycle both active. Step 3b of Let an investor choose custody shows calls 4 to 7 with their requests and responses.

Step 4: create the identity

Call GET /api/v2/system/identities/me with the participant, custody account and network in the request context headers. It answers 200 with status set to NOT_PROVISIONED for a wallet with no identity, and 404 with IDENTITY_SCOPE_NOT_FOUND while the network's identity registry is not indexed yet. Treat both as no identity. In that case call POST /api/v2/identities with the wallet address and walletVerification. The first call is refused with a 409, DALP-12389, whose data.signingPayload.typedData holds the identity factory's typed data. The investor signs it with their Privy wallet through eth_signTypedData_v4. Repeat the call with externalMessageAuthorization: { "signature": "<signature>" }, then wait for the write to settle. Idempotency and async settlement covers the settlement signal. Registration and verification follow Onboard an investor through the API from step 3.

Step 5: deploy the smart wallet

Skip this step when aaEnabled is false. Otherwise call POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment with no body fields. The first call is refused with a 409 whose data.signingPayload.typedData holds the typed data to sign. The investor signs that typed data with their Privy wallet. Repeat the call with externalMessageAuthorization: { "signature": "<signature>" }. The response names the smart wallet address. The wallet needs no native currency for this step.

Step 6: send a sponsored transfer

A transfer is three calls. First prepare it: POST /api/v2/transaction-preparations with the custodyAccountId, executionMode set to prefer-aa, a summary and the operation (kind, contractAddress, callData and an optional value). The response carries preparedUserOperation with a userOperationHash and the previewData of the operation. Where the organisation's gas payer is the paymaster, previewData names the paymaster and carries its paymasterData, so the smart wallet pays nothing. The paymaster ticket in paymasterData expires after a window the deployment operator sets, 300 seconds by default, so sign and finalise within that window or prepare again.

Then the investor signs. Build the EIP-712 typed data of the user operation from previewData for the EntryPoint in previewData.entryPointAddress (the viem getUserOperationTypedData helper builds it), check that its hash equals userOperationHash, and sign it with eth_signTypedData_v4 in Privy. A plain message signature fails, because it adds a text prefix.

Last, finalise: POST /api/v2/transaction-preparations/:preparationId/transitions with the finalize action and this artifact in its body.

{
  "action": "finalize",
  "artifact": {
    "_tag": "Eip4337UserOperationAuthorization",
    "kind": "eip-4337-user-operation",
    "userOperationHash": "0x...",
    "signature": "0x..."
  }
}

The response carries a transactionRequestId. Follow it on the transaction status stream, GET /api/v2/transaction-requests/:transactionId/stream, until it reaches a terminal state.

Which provider is offered

The deployment decides which providers exist. The switch custody.providers.<kind>.enabled turns a provider on for the whole deployment. Each organisation then decides which enabled providers its investors see: a provider configuration is offered when its offeredToInvestors flag is set, and the configuration with the investorProvider flag is the default. Offering Privy alone is a configuration change and needs no code.

Errors

Steps 3 to 5 return the refusals in The refusals each step returns.

Code
Status
When it appears
What to do
422
The organisation does not sponsor gas and the smart wallet holds too little to pay for the prepared transfer
Send at least shortfallWei of native currency to the smart wallet, then prepare the transfer again
400
A signing request carried no walletVerification block
Add walletVerification and repeat the request
409
The request conflicts with how the preparation is bound, its idempotency key names another preparation, or the smart wallet already has an open owner-signed operation
Read the preparation and send the claim, attachment or artifact that matches it. Use a new idempotency key for a different operation, or finish or expire the open one

DALP error index lists every code. Read and handle DALP errors covers the envelope these codes arrive in.

Next steps