# Embed a Privy wallet

Source: https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet
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 [#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](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) covers managed custody, where the platform holds the key. [Let an investor choose custody](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody) covers the choice between custody kinds and the browser-wallet path. [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/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 [#the-sequence-at-a-glance]

[Figure] 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. Labels: 1 Read the offered providers; GET /api/v2/custody/investor-provider returns the Privy app id; 2 Sign the investor in to Privy; GET /api/auth/token, checked by Privy against GET /api/auth/jwks; 3 Create the participant and prove the key; POST /api/v2/participants, then the control preparation and lifecycle calls; 4 Create the identity; GET /api/v2/system/identities/me, then POST /api/v2/identities when it is missing; 5 Deploy the smart wallet; POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment; 6 Prepare the transfer; POST /api/v2/transaction-preparations returns the user operation to sign; 7 Sign in Privy and finalise; POST /api/v2/transaction-preparations/:preparationId/transitions (finalise), paid by the paymaster.

_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-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](#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 [#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 [#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](#which-provider-is-offered).

## Step 2: sign the investor in to Privy [#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 [#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](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody#step-3b-run-the-browser-wallet-branch) shows calls 4 to 7 with their requests and responses.

## Step 4: create the identity [#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](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), 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](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the settlement signal. Registration and verification follow [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) from step 3.

## Step 5: deploy the smart wallet [#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 [#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.

```json
{
  "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 [#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 [#errors]

Steps 3 to 5 return the refusals in [The refusals each step returns](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody#the-refusals-each-step-returns).

| Code | Status | When it appears | What to do |
| --- | --- | --- | --- |
| [DALP-14034](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 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 |
| [DALP-12033](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 400 | A signing request carried no `walletVerification` block | Add `walletVerification` and repeat the request |
| [DALP-14033](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 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](https://docs.settlemint.com/docs/api-reference/errors) lists every code. [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the envelope these codes arrive in.

## Next steps [#next-steps]

- [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) registers the identity and verifies the investor.
- [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the headers and status URLs these writes use.
- [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains what each provider holds.
