Skip to main content

Let an investor choose custody

Read the custody options an organisation offers after sign-up, then run the managed-account or browser-wallet branch to one account.

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.

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

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.Read the options, show the choice, then one of two branches to one account.The same figure as the desktop variant, drawn portrait for phone widths. Read the offered options and the default, render the choice, then take one branch. The managed branch is one create that the provider fills in, with a statusUrl to follow when it answers 202. The browser-wallet branch creates a pending participant, proves the key by signing a prepared message, creates the identity when the account holds none, and deploys the smart wallet where the organisation runs smart wallets. Both branches end on one participant, one account and one identity.

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

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

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:

{
  "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.

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
browser-wallet
An account whose key stays in a wallet the investor connects
embedded-wallet
An account whose key sits in an embedded wallet that a provider such as Privy creates inside your front end

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 "https://your-instance.example.com/api/v2/organizations/019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80/joinable-participants" \
  -H "X-Api-Key: sm_dalp_..."

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

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 describes.

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" }
  }'

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

{
  "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.

{
  "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 covers both headers.

A 403 with DALP-14228 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

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 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

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

{
  "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

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"
  }'
{
  "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:

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

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\"}"
    }
  }'

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:

{ "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, 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.

curl "https://your-instance.example.com/api/v2/users/me/custody-account-requests/019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40" \
  -H "X-Api-Key: sm_dalp_..."
{
  "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

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, means the network's registry is not indexed yet, not that the call failed.

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"

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:

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" }
  }'
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, 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:

{
  "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: send the call once more with no signature and sign the fresh payload. A wallet that already holds an identity contract answers DALP-14227, 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 from step 3.

5. Deploy the smart wallet

Skip the step when aaEnabled is false. Otherwise:

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 '{}'

The first call is refused on purpose. It answers 409 with DALP-12388, whose data carries the account and the payload to sign:

{
  "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:

{ "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.

{
  "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

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 or DALP-12388 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

Code
Where it appears
What it means and what to do
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.
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.
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.
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.
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.
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.
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.
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.
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.
Browser wallet, deploying the smart wallet
A deployment check refused it and data.reason names which. The reasons are below.
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 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

Was this page helpful?