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
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.
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:
privycarries atenant, anappIdand, when the configuration sets one, aclientId.dynamiccarries atenantand anenvironmentId.injected-browsercarries 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/ 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_. 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- 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/, 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- 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 { request. 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/ 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_ and reason: "NO_, 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. 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
aaEnabledistrue.
So the sequence for a wallet that does not sign typed data is:
- Send the identity or deployment call with no signature. Read
signingPayloadfrom the DALP-12389 or DALP-12388 refusal. - Attempt the
eth_signTypedData_v4signature. 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. - 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.
- 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
data.reason on DALP-12387 is one of:
account-: the organisation runs no smart wallets on this network. Nothing is wrong, so treat the investor as set up and stop.abstraction- disabled 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
- Embed a Privy wallet in an investor front end takes the
embedded-walletbranch through the Privy sign-in and a sponsored transfer. - Onboard an investor through the API registers the identity and verifies the investor once the account exists.
- Custody Provider explains what a managed configuration holds and what a self-custody configuration never holds.
- Smart Wallet explains what the account step 5 deploys, and what it enforces.