# Investor custody choice

Source: https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody
Read the custody options an organisation offers after sign-up, then run the managed-account or browser-wallet branch to one account.

## Overview [#overview]

A signed-in investor who joins an organisation picks how they hold: an account an organisation runs and signs for them, or an account whose key stays in a wallet they control. Your front end reads the options the organisation offers, renders them, and then runs one of two call sequences. Both end on one participant, the platform's record of one compliance subject, one custody account on the network, and one on-chain identity, the contract that holds the investor's claims. Where the organisation runs smart wallets, the account is a smart wallet.

**Where this fits.** [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) covers an operator creating a user, registering an identity and verifying the investor with a know-your-customer provider. [Embed a Privy wallet in an investor front end](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) covers the embedded-wallet branch in full, including the Privy sign-in and sponsored transfers. This page covers the choice itself and the two other branches, so it does not repeat either.

The custody kind is decided by the provider configuration the investor picks, not by a field your front end sends. The read that offers the choice is per organisation and per network, so an organisation that runs on several networks offers a slightly different list on each, and the `X-Chain-Id` request header names the network.

## The sequence at a glance [#the-sequence-at-a-glance]

[Figure] Read the options, show the choice, then one of two branches to one account. Step 1 reads GET /api/v2/custody/investor-provider, which answers data, the organisation's default, and options, every configuration it offers to investors. Step 2 renders those options as they arrive, each carrying a custody label of managed-account, embedded-wallet or browser-wallet. The branches split. On the managed account, one call to POST /api/v2/participants has the provider create the account and its identity, and when the create outlasts the wait, a 202 answer sends the caller to the statusUrl. On the browser wallet, POST /api/v2/participants answers 201 with a pending participant, the investor signs a control message that POST /api/v2/users/me/custody-accounts/lifecycles turns into an active account, the identity is created with POST /api/v2/identities after the same key signs the identity factory payload, and where the organisation runs smart accounts the smart wallet is deployed with POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment, signed by that same key. Both branches rejoin on one participant, one custody account and one identity. Labels: 1 Read what the organisation offers; GET /api/v2/custody/investor-provider answers data and options; 2 Show the choice; each option carries custody: managed-account, embedded-wallet or browser-wallet; managed-account; browser-wallet; 3a Create the participant; POST /api/v2/participants; the provider creates the account and its identity; 3b Create the participant; POST /api/v2/participants; 201: activationStatus pending, no key yet; If it answers 202; follow Location: statusUrl; until the request is terminal; Prove the key; personal_sign the prepared message, then POST; /api/v2/users/me/custody-accounts/lifecycles; Create the identity; POST /api/v2/identities; only when the account holds none; Deploy the smart wallet; POST …/smart-wallet-deployment; sign the factory typed data, then repeat; both branches land here; One outcome; one participant, one account on the network, one on-chain identity; a smart wallet when the organisation runs them.

_Steps 1 and 2 happen before any account exists. Step 3a and step 3b are alternatives, and step 3b runs only for a browser-wallet option._

## Step 1: read the options and the default [#step-1-read-the-options-and-the-default]

**cURL**

```bash
curl "https://your-instance.example.com/api/v2/custody/investor-provider" \
  -H "X-Api-Key: sm_dalp_..."
```

**TypeScript SDK**

```ts
const answer = await dalp.api.custodyProviderConfigurationsV2.investorProvider();

answer.data; // the organisation's default, or null
answer.options; // every configuration offered to investors, the default first
```

The answer carries two fields. `data` is the organisation's designated investor provider, or `null` when no configuration carries that designation. `options` is every configuration the organisation offers to investors, the default first. Each entry in either place holds the same shape:

```json
{
  "data": {
    "id": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e",
    "provider": "local",
    "custody": "managed-account",
    "capabilities": {
      "accountCreation": "create",
      "accountProof": "provider-attestation",
      "managedSigning": true,
      "externalSigning": false,
      "accountAbstraction": true,
      "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }]
    },
    "clientConfiguration": null
  },
  "options": [
    {
      "id": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e",
      "provider": "local",
      "custody": "managed-account",
      "capabilities": {
        "accountCreation": "create",
        "accountProof": "provider-attestation",
        "managedSigning": true,
        "externalSigning": false,
        "accountAbstraction": true,
        "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }]
      },
      "clientConfiguration": null
    },
    {
      "id": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c",
      "provider": "injected-browser",
      "custody": "browser-wallet",
      "capabilities": {
        "accountCreation": "link",
        "accountProof": "interactive-address",
        "managedSigning": false,
        "externalSigning": true,
        "accountAbstraction": true,
        "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }]
      },
      "clientConfiguration": { "kind": "injected-browser" }
    }
  ]
}
```

The response has already dropped every configuration the deployment has switched off, every configuration the organisation does not offer to investors, and every configuration that cannot activate an account on the request's network. Render `options` as it arrives. A front end that recomputes the list from its own configuration table, or picks its own default, disagrees with the platform the moment an operator changes the offer.

The read answers for the network the request names in `X-Chain-Id`, and for the organisation's default network when the request names none. A front end that sends no chain header therefore offers, and onboards on, the organisation's default network even when the organisation runs on several. Send `X-Chain-Id` only when your front end shows a network picker of its own.

`data` being `null` does not mean the investor has no choice. It means no configuration is designated as the default, and `options` can still hold several. An empty `options` means the organisation offers no custody to investors. Show a blocked screen that says an administrator has to offer one in [Custody](https://docs.settlemint.com/docs/console/organisation-settings/custody).

## Step 2: render the choice [#step-2-render-the-choice]

Each entry's `custody` field is the label to show, and it names the branch to run. The platform derives it from the provider, so your front end maps nothing.

| `custody` | What the investor gets | Branch |
| --- | --- | --- |
| `managed-account` | An account the organisation holds the key for and signs with | [Step 3a](#step-3a-run-the-managed-account-branch) |
| `browser-wallet` | An account whose key stays in a wallet the investor connects | [Step 3b](#step-3b-run-the-browser-wallet-branch) |
| `embedded-wallet` | An account whose key sits in an embedded wallet that a provider such as Privy creates inside your front end | [Embed a Privy wallet](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) |

Mark the entry whose `id` matches `data.id` as the default rather than ordering the list yourself. With one option you can skip the choice and run its branch. With none, show the blocked screen.

The `clientConfiguration` field tells an embedded-wallet branch which client SDK to load, and its `kind` decides:

- `privy` carries a `tenant`, an `appId` and, when the configuration sets one, a `clientId`.
- `dynamic` carries a `tenant` and an `environmentId`.
- `injected-browser` carries nothing else, because the wallet is one the investor already holds.

Every managed configuration returns `null` here, because the key sits with a custodian rather than in a front end.

The choice decides the first key, not the only one. A custody account's lifecycle offers `onboard`, `add`, `revoke` and `archive`, and none of them moves an existing account to another provider or swaps the key that controls it. The participant can `add` another custody account later, and where the organisation runs smart wallets, the added account becomes another signer on the same smart wallet. Any one signer can act for the smart wallet alone, and `revoke` removes a signer. Show that consequence on each option.

Before any branch, read the organisation's network and account-abstraction settings:

**cURL**

```bash
curl "https://your-instance.example.com/api/v2/organizations/019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80/joinable-participants" \
  -H "X-Api-Key: sm_dalp_..."
```

**TypeScript SDK**

```ts
const joinable = await dalp.api.party.participantJoins.joinableList({
  organizationId: "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80",
});

joinable.data.chainId; // the network this investor is created on
joinable.data.aaEnabled; // true when the organisation runs smart wallets
joinable.data.pendingParticipantId; // the participant to resume, or null
```

`aaEnabled` is the flag that decides whether a smart wallet is part of the outcome. `pendingParticipantId` is the participant an interrupted browser-wallet sign-up left behind, and step 3b resumes it instead of creating a second one.

## Step 3a: run the managed-account branch [#step-3a-run-the-managed-account-branch]

One call creates everything. The provider mints the account, and the platform creates the participant's identity with it. The investor's own session sends this call. An identity manager who creates the user for the investor calls `POST /api/v2/users` instead, as [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) describes.

**cURL**

```bash
curl -X POST "https://your-instance.example.com/api/v2/participants" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "Idempotency-Key: participant-amara-managed-001" \
  -H "Prefer: wait=60" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80",
    "providerConfigurationId": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e",
    "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }
  }'
```

**TypeScript SDK**

```ts
const created = await dalp.api.party.participants.create(
  {
    organizationId: "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80",
    providerConfigurationId: "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e",
    walletVerification: { verificationType: "PINCODE", secretVerificationCode: "482913" },
  },
  { headers: { "Idempotency-Key": "participant-amara-managed-001" } }
);
```

A settled create answers `200` with the participant, the account the provider made and the identity created for it:

```json
{
  "data": {
    "id": "pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35",
    "kind": "smart",
    "wallet": "0x4F1bE7dB9d2c3aA85f6C1E0b7D9a2C4e6F8b0A1c",
    "identity": "0x9A2c4E6f8B0a1C3d5E7f9A2c4E6f8B0a1C3d5E7f"
  },
  "links": {
    "self": "/v2/participants/pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35"
  }
}
```

`kind` is `"smart"` when the organisation runs smart wallets on that network and `"eoa"` when it does not, so the managed branch needs no deployment call of its own: `aaEnabled` and `kind` agree, and the account arrives as a smart wallet already. `wallet` is the address the investor holds assets at, `identity` is the identity contract created for the participant.

A create that is still running when the platform's wait ends, or that the provider has not finished provisioning, answers `202` instead. The body carries `requestId`, `state` and `statusUrl`, and the `Location` header holds the same `statusUrl`. The SDK's response type then holds no `id` at all. The create keeps running after that answer: the platform still opens the custody account, deploys the smart wallet where the organisation runs smart wallets, and creates the identity. Follow `statusUrl` until the request reads `succeeded`. A create whose provisioning request has ended `failed` or `expired` answers `DALP-14226`, and its message names the request that ended.

```json
{
  "requestId": "3b7e91c4-52d0-8a6f-9c13-47e2d8a05b6f",
  "state": "awaiting-proof",
  "statusUrl": "/api/v2/participants/pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35/custody-account-requests/3b7e91c4-52d0-8a6f-9c13-47e2d8a05b6f"
}
```

Read `statusUrl` until its `data.state` is `succeeded`, `failed` or `expired`. While the create runs, the state is `awaiting-proof` or the custody account's own progress, such as `claimed`. On this branch `awaiting-proof` only means that the create is still running, because a managed account needs no proof from the investor. The state is `succeeded` only once the identity exists as well, and then the same create with the same `Idempotency-Key` answers the `200` with the participant. A failed create reads `failed` with the `terminalCode` `PARTICIPANT_CREATE_FAILED`. When the custody account request itself ended `failed` or `expired`, the status keeps that request's own `terminalCode`.

A create that finishes inside the wait while its custody account request has not settled also answers `202`, with that request's state, such as `claimed`, in place of `awaiting-proof`. A create whose custody account request ended `failed` or `expired` answers `DALP-14226`.

Unlike most writes, which answer `202` at once without a `wait` directive, this create waits by default. Without a `wait` directive in the `Prefer` header, `Prefer: respond-async` included, the platform holds the answer for the longest wait, 99 seconds. `Prefer: wait=N` sets that wait to `N` seconds instead, clamped between 5 and 99. Every wait also ends five seconds before the request deadline that the deployment configures, so the platform answers `202` before that deadline cuts the request off with an error. With the default 60 second deadline, the answer is therefore held for up to 55 seconds. The wait never changes the outcome of the create, only whether the participant arrives in a `200` or behind a `202`.

The `Idempotency-Key` makes a retry safe and lets the investor resume a create their browser abandoned: the same key joins the same run and answers the same participant instead of a second one. A create sent without the header gets a new key from the platform on every attempt, so its retry cannot join the earlier run. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers both headers.

A `403` with [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) on this call means the caller may not create this participant. The investor's own session creates its own participant. To name another user in `forUserId`, the caller signs in through the browser as themselves and holds the identity-manager role, and that user must be a member of the organisation. An API key or an impersonated session cannot name another user. Once the organisation offers at least one configuration to investors, choosing a configuration it does not offer also needs the identity-manager role. When that configuration cannot hold a managed account, such as a browser wallet, the platform refuses it for every caller, because its key could never be proven.

## Step 3b: run the browser-wallet branch [#step-3b-run-the-browser-wallet-branch]

The branch takes up to three signatures from the investor's wallet: the control message, the identity factory payload and, where the organisation runs smart wallets, the account factory payload. The control message is a personal message. The two factory payloads are EIP-712 typed data that the wallet signs with `eth_signTypedData_v4`, and [When the wallet cannot sign typed data](#when-the-wallet-cannot-sign-typed-data) covers a wallet that cannot. The participant exists first and cannot act anywhere until its key is proven.

### 1. Create the participant in a pending state [#1-create-the-participant-in-a-pending-state]

Use the same call as the managed branch, with the browser-wallet configuration's `id`. It answers `201` with a pending participant:

```json
{
  "data": {
    "id": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
    "activationStatus": "pending",
    "proofUrl": "/api/v2/users/me/custody-account-control-preparations"
  },
  "links": {
    "self": "/v2/participants/pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63"
  }
}
```

`activationStatus` is always `pending` here: the key is not proven, so the participant holds no wallet and the organisation can drive nothing for it. Never render that as a failure. It is the state the next call resolves, and a sign-up that stops now resumes as `pendingParticipantId` on the joinable read. Prove the key with the two calls below. `proofUrl` names the first of them, `POST /api/v2/users/me/custody-account-control-preparations`, which the participant's owner calls in a session of their own.

### 2. Prepare the control message and sign it [#2-prepare-the-control-message-and-sign-it]

**cURL**

```bash
curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-account-control-preparations" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "Content-Type: application/json" \
  -d '{
    "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c",
    "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
    "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
  }'
```

**TypeScript SDK**

```ts
const prepared = await dalp.api.custodyUserAccountsV2.prepareControl({
  providerConfigurationId: "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c",
  participantId: "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
  signingAddress: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
});

prepared.data.message; // the text to sign
prepared.data.binding; // echo this back against the proof
prepared.data.account; // the account facts the proof binds
```

```json
{
  "data": {
    "binding": {
      "operation": "onboard",
      "actorUserId": "019c4d2e-2b9f-7d51-8a4e-1c0d2e3f4a5b",
      "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
      "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80",
      "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c",
      "ledger": "evm",
      "network": "31337",
      "domain": "your-instance.example.com",
      "origin": "https://portal.example.com",
      "audience": "dalp",
      "participantResetEpoch": 0,
      "nonce": "4b1d2f3a5c6e7d8b9a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a",
      "proofDigest": "0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9",
      "expiresAt": "2026-11-05T10:22:14.000Z"
    },
    "account": {
      "_tag": "EvmCustodyAccountFacts",
      "ledger": "evm",
      "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
      "managedSignerKeyId": null
    },
    "message": "DALP custody-account control proof\n0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9"
  },
  "links": {
    "self": "/v2/users/me/custody-account-control-preparations"
  }
}
```

Sign `message` exactly as it arrives, as a personal message: `personal_sign` over its UTF-8 bytes, with the address in `account.signingAddress`. The text is two lines, the label `DALP custody-account control proof` and the `proofDigest`, and the platform rebuilds it from the binding and the account to check your signature. A browser wallet signs it with `personal_sign`, the message as hex-encoded UTF-8 bytes and the account address:

```ts
await provider.request({ method: "personal_sign", params: [toHex(message), address] });
```

A wallet that signs a hash of the text, or an `eth_sign` digest instead, produces a signature that recovers to nobody and the platform refuses it.

A prepared control expires 15 minutes after the platform issues it, so prepare and sign in one flow rather than resuming a stored one.

### 3. Hand the proof back [#3-hand-the-proof-back]

**cURL**

```bash
curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-accounts/lifecycles" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "Idempotency-Key: custody-proof-amara-001" \
  -H "Content-Type: application/json" \
  -d '{
    "proof": {
      "binding": {
        "operation": "onboard",
        "actorUserId": "019c4d2e-2b9f-7d51-8a4e-1c0d2e3f4a5b",
        "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
        "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80",
        "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c",
        "ledger": "evm",
        "network": "31337",
        "domain": "your-instance.example.com",
        "origin": "https://portal.example.com",
        "audience": "dalp",
        "participantResetEpoch": 0,
        "nonce": "4b1d2f3a5c6e7d8b9a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a",
        "proofDigest": "0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9",
        "expiresAt": "2026-11-05T10:22:14.000Z"
      },
      "account": {
        "_tag": "EvmCustodyAccountFacts",
        "ledger": "evm",
        "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
        "managedSignerKeyId": null
      },
      "artifact": "{\"kind\":\"evm-self-custody-control\",\"signature\":\"0x8f2a...c41d\"}"
    }
  }'
```

**TypeScript SDK**

```ts
const started = await dalp.api.custodyUserAccountsV2.runLifecycle(
  {
    proof: {
      binding: prepared.data.binding,
      account: prepared.data.account,
      artifact: JSON.stringify({ kind: "evm-self-custody-control", signature }),
    },
  },
  { headers: { "Idempotency-Key": "custody-proof-amara-001" } }
);
```

`binding` and `account` go back exactly as step 2 returned them. They are the values the signature covers. `artifact` is a JSON string rather than an object, in this shape:

```json
{ "kind": "evm-self-custody-control", "signature": "0x8f2a...c41d" }
```

A signature that does not bind the participant, the organisation and the prepared nonce is refused with [DALP-12274](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), and the fix is a fresh preparation.

The answer is the lifecycle request: `200` with the request body and its `requestId`, or `202` with `{ requestId, state, statusUrl }`. Follow it.

```bash
curl "https://your-instance.example.com/api/v2/users/me/custody-account-requests/019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40" \
  -H "X-Api-Key: sm_dalp_..."
```

```json
{
  "data": {
    "requestId": "019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40",
    "custodyAccountId": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a",
    "organizationContextId": "019c4d2e-e5f6-7b78-8c90-9d120e1f2a3b",
    "operation": "onboard",
    "state": "succeeded",
    "externalPreparationId": null,
    "externalAuthorizingCustodyAccountId": null,
    "externalMessageDigest": null,
    "terminalResult": "succeeded",
    "terminalCode": null
  },
  "links": {
    "self": "/v2/users/me/custody-account-requests/019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40"
  }
}
```

A request moves through working states such as `awaiting-proof`, `claimed` and `reconciling`, and ends `succeeded`, `failed` or `expired`. The same path with `/stream` appended reports each change instead of you polling, and it answers only a same-origin request. A request that ends `failed` or `expired` names why in `terminalCode`, and `DALP-14219` on a repeated attempt means another request moved the account under you.

The account is live when the participant's custody account list shows both `accountLifecycle` and `contextLifecycle` as `active`. Its `signingAddress` is the wallet the investor just proved.

### 4. Create the identity [#4-create-the-identity]

Read first. `GET /api/v2/system/identities/me` answers whether the participant already holds an identity on this network, and a `404` with `IDENTITY_SCOPE_NOT_FOUND`, [DALP-14224](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), means the network's registry is not indexed yet, not that the call failed.

**cURL**

```bash
curl "https://your-instance.example.com/api/v2/system/identities/me" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \
  -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \
  -H "X-Chain-Id: 31337"
```

**TypeScript SDK**

```ts
const identity = await dalp.api.party.identities.me(
  {},
  {
    headers: {
      "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
      "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a",
      "X-Chain-Id": "31337",
    },
  }
);
```

An answer with `status: "NOT_PROVISIONED"` and `reason: "NO_IDENTITY"`, or a `404` with `IDENTITY_SCOPE_NOT_FOUND`, both mean no identity yet. Then create it with the account's `signingAddress`:

```bash
curl -X POST "https://your-instance.example.com/api/v2/identities" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \
  -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \
  -H "X-Chain-Id: 31337" \
  -H "Idempotency-Key: identity-amara-001" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
    "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }
  }'
```

```ts
await dalp.api.party.identities.create(
  {
    wallet: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
    walletVerification: { verificationType: "PINCODE", secretVerificationCode: "482913" },
  },
  {
    headers: {
      "Idempotency-Key": "identity-amara-001",
      "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
      "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a",
      "X-Chain-Id": "31337",
    },
  }
);
```

The platform holds no key for this account, so the first create is refused on purpose. It answers `409` with [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), and `data.signingPayload.typedData` carries the identity factory's EIP-712 payload for that wallet. Sign it with `eth_signTypedData_v4` on the wallet in `account.signingAddress`, then repeat the same call with the signature added to the body:

```json
{
  "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
  "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" },
  "externalMessageAuthorization": { "signature": "0x9b3e…41c7" }
}
```

A signature that does not recover to that wallet over the payload this request prepared answers [DALP-12390](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors): send the call once more with no signature and sign the fresh payload. A wallet that already holds an identity contract answers [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), so read the identity rather than repeating the create. Registering the identity on the network and verifying the investor with a know-your-customer provider continue in [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) from step 3.

### 5. Deploy the smart wallet [#5-deploy-the-smart-wallet]

Skip the step when `aaEnabled` is `false`. Otherwise:

**cURL**

```bash
curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-accounts/019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a/smart-wallet-deployment" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \
  -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \
  -H "X-Chain-Id: 31337" \
  -H "Prefer: wait=30" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**TypeScript SDK**

```ts
const headers = {
  "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63",
  "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a",
  "X-Chain-Id": "31337",
};

const deployment = await dalp.api.custodyUserAccountsV2.deploySmartWallet(
  { custodyAccountId: "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" },
  { headers }
);

deployment.data.deployed; // false until the wallet's code is on the network and indexed
```

The first call is refused on purpose. It answers `409` with [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), whose `data` carries the account and the payload to sign:

```json
{
  "code": "custody.smart-wallet.EXTERNAL_MESSAGE_AUTHORIZATION_REQUIRED",
  "status": 409,
  "message": "The smart wallet needs a signature from its controlling wallet",
  "data": {
    "id": "DALP-12388",
    "category": "domain",
    "retryable": false,
    "why": "The custody account in `data.custodyAccountId` is self custody, so the platform holds no key for it. Its wallet has to sign the account factory payload in `data.signingPayload` before the smart wallet can be deployed.",
    "fix": "Sign `data.signingPayload` with that account's connected wallet, then send the same request again with the signature in `externalMessageAuthorization`.",
    "custodyAccountId": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a",
    "signingPayload": {
      "_tag": "TypedDataSigningPayload",
      "kind": "eip-712",
      "typedData": {
        "domain": {
          "name": "DALPAccountFactory",
          "version": "1",
          "chainId": 31337,
          "verifyingContract": "0x0000000000000000000000000000000000000fac"
        },
        "types": {
          "EIP712Domain": [
            { "name": "name", "type": "string" },
            { "name": "version", "type": "string" },
            { "name": "chainId", "type": "uint256" },
            { "name": "verifyingContract", "type": "address" }
          ],
          "CreateAccount": [
            { "name": "owner", "type": "address" },
            { "name": "validator", "type": "address" },
            { "name": "onchainID", "type": "address" },
            { "name": "salt", "type": "bytes32" },
            { "name": "deadline", "type": "uint256" },
            { "name": "validatorInitDataHash", "type": "bytes32" }
          ]
        },
        "primaryType": "CreateAccount",
        "message": {
          "owner": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
          "validator": "0x0000000000000000000000000000000000000000",
          "onchainID": "0x0000000000000000000000000000000000000000",
          "salt": "0x3f1c5a7e9b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d1f3a",
          "deadline": 1900000000,
          "validatorInitDataHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
        }
      }
    }
  }
}
```

Sign the typed data in `signingPayload` with `eth_signTypedData_v4` on the wallet its `message.owner` names, then repeat the same call with the signature:

```json
{ "externalMessageAuthorization": { "signature": "0x4c1d…8f2a" } }
```

Repeat the same signature while `deployed` is `false`. Check the account `data` names before you sign: a challenge for another custody account is a challenge you must not sign, and signing it hands a signature over a second account's deployment to whichever page asked.

```json
{
  "data": {
    "walletId": "019c4d2e-f7a8-7c90-8d12-9e340f1a2b3c",
    "address": "0x4F1bE7dB9d2c3aA85f6C1E0b7D9a2C4e6F8b0A1c",
    "deployed": true
  },
  "links": {
    "self": "/v2/users/me/custody-accounts/019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a/smart-wallet-deployment"
  }
}
```

`address` is the smart wallet the investor holds assets at. The wallet has code on the network only when `deployed` is `true`, so read this field rather than the response arriving at all. `Prefer: wait=N` holds the answer up to `N` seconds until the network confirms.

## When the wallet cannot sign typed data [#when-the-wallet-cannot-sign-typed-data]

A browser wallet can connect, hold its key and sign the control message, which is a personal message, and still be unable to produce the EIP-712 signatures that step 4 and step 5 need. The investor is then half set up.

- **Only that account's own key can sign.** Both signatures must recover to the wallet address the investor proved in step 3b.2. A second wallet cannot sign for it, and no route swaps the key of an existing account.
- **The participant and the account are already real.** Steps 3b.1 to 3b.3 succeeded, so the investor keeps their participant and an active custody account. A refused signature does not undo them, and the investor does not start again.
- **The identity step needs typed data on every network.** Without an identity the wallet cannot be registered, so it cannot hold a regulated token. The deployment step adds a second typed-data signature only where `aaEnabled` is `true`.

So the sequence for a wallet that does not sign typed data is:

1. Send the identity or deployment call with no signature. Read `signingPayload` from the [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) or [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) refusal.
2. Attempt the `eth_signTypedData_v4` signature. If the wallet raises "method not supported", declines, or returns something that is not a 65-byte hex string, stop there: do not retry the signature, and do not ask the investor to connect a different wallet for this step.
3. Leave the participant and the account alone, and tell the investor which step waits on their wallet. An account with no identity cannot hold a regulated token. An account with an identity and no smart wallet lacks the smart wallet the organisation's feature set assumes.
4. Recover later by getting a signature from that same key. A wallet that gained EIP-712 support in an update, or the same key restored in a wallet that supports it, can sign a fresh payload, and the step completes against the account that already exists.

Prevent the failure at the choice step. A wallet announces itself with a name, an icon and a reverse DNS identifier, and none of those says whether it signs typed data, so there is nothing to read in advance. Probe when the investor chooses: ask the wallet to sign a typed-data payload whose domain names your own app and no DALP contract, tell the investor on screen that the signature moves nothing, and offer only the wallets that answered. Never reuse a payload from a DALP-12388 or DALP-12389 refusal as the probe. For a wallet that does not, show the `managed-account` option instead, where the organisation offers one. That costs the investor a decision at a moment when nothing is created, rather than a broken outcome afterwards.

## The refusals each step returns [#the-refusals-each-step-returns]

| Code | Where it appears | What it means and what to do |
| --- | --- | --- |
| [DALP-12271](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (422) | Either branch, on the create | The configuration does not exist in this organisation, or the deployment does not enable its provider or ledger. Read the options again and use an offered `id`. |
| [DALP-14226](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Either branch, on the create | Another participant holds that key or user link, the configuration is switched off for new accounts or not healthy, a bring-your-own-key create asked for a `smart` wallet type, or the provisioning request ended `failed` or `expired`. Read the message. If the provisioning request ended `failed` or `expired`, repeat the create with a new `Idempotency-Key`. Otherwise read the participant or the options and continue from there. |
| [DALP-12274](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (422) | Browser wallet, returning the proof | The proof, the account facts or the artifact does not bind this participant and ledger. Prepare a new control and sign it. |
| [DALP-14219](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, the lifecycle call or the deployment | Another request moved this account's lifecycle. Read the account and take the step its current state allows. Do not repeat the call. |
| [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, creating the identity | An identity contract already exists, another write on that wallet is in flight, or the participant already holds or is creating a registry entry for another wallet. Read the identity instead of creating it. |
| [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, creating the identity | The account's wallet must sign the identity factory payload. Sign the typed data in `signingPayload` and repeat the call with the signature. |
| [DALP-12390](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (400) | Browser wallet, creating the identity | The signature does not recover to the account's wallet over the payload this request prepared. Send once more with no signature, sign the payload you get back, and repeat. |
| [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, deploying the smart wallet | The account's controller must sign the factory payload. Sign the typed data in `signingPayload` and repeat the call with the signature. |
| [DALP-12386](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (400) | Browser wallet, deploying the smart wallet | The signature does not recover to the controller over the payload this request prepared, or its deadline passed. Send once more with no signature, sign the payload you get back, and repeat. |
| [DALP-12387](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, deploying the smart wallet | A deployment check refused it and `data.reason` names which. The reasons are below. |
| [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) (403) | Either branch, on the create | The caller may not create this participant: it named another user without signing in through the browser as itself with the identity-manager role, or it chose a configuration the organisation does not offer to investors. Create with the investor's own session and an offered `id`. |

`data.reason` on [DALP-12387](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) is one of:

- `account-abstraction-disabled`: the organisation runs no smart wallets on this network. Nothing is wrong, so treat the investor as set up and stop.
- `factory-missing`: the network's directory publishes no account factory, or provisioning the wallet failed for that reason. Report it, because no signature clears it and every browser-wallet investor on that network stops at the same step.
- `controller-unauthorized`: the participant already holds a smart wallet on this network, or its signing key, and another account controls it. Read the participant's smart wallet before retrying, because a repeat cannot make this account the controller.
- `prediction-mismatch`: the factory predicts another address for this wallet, or the recorded smart wallet sits somewhere else. Read the account instead of retrying, because the address the investor would end up with is not the one on record.
- `deadline-expired`: the deployment was still pending when its authorisation expired. Request a fresh payload and sign it again.
- `deploy-failed`: the deployment settled and the wallet still has no code on the network. Retry with the same signature, and if it repeats, report the network's answer rather than looping.

A refused deployment is not worth more than a few attempts in one session. `deploy-failed` and an unconfirmed deployment look identical from a polling loop, and neither changes by polling faster.

## Next steps [#next-steps]

- [Embed a Privy wallet in an investor front end](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) takes the `embedded-wallet` branch through the Privy sign-in and a sponsored transfer.
- [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) registers the identity and verifies the investor once the account exists.
- [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains what a managed configuration holds and what a self-custody configuration never holds.
- [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) explains what the account step 5 deploys, and what it enforces.
