Create a wallet

Set up a custody provider and create wallets for signing transactions.

A custody wallet is a Solana keypair whose key a custody provider holds. SDP sends each signing request to the provider that holds the wallet's key. Set up a provider, then create wallets for transfers and other onchain operations.

Supported providers

ProviderDescription
PrivyEmbedded wallet infrastructure platform

1. Open wallet setup

Open Wallets in the sidebar and click Create wallet. Once the project has wallets, the button reads Create a wallet. Only organization admins can create wallets. Complete organization setup first if needed.

2. Choose a custody provider

Under Which provider holds the keys?, select a provider. You can't change it after the wallet exists. When your project can use only one provider, it's already selected and the question isn't shown.

3. Choose whose account holds the keys

If you can connect your own account for the provider, the page asks whose account holds the keys, such as Whose Privy account holds the keys? When there's a choice, nothing is preselected.

  • SDP's Privy account: for trying SDP out in Sandbox. It has no wallet policy and nothing to set up.
  • Your own Privy account: connect it with its app ID and secret. Production accepts only your own account; see Go to mainnet.

4. Create the first wallet

With SDP's account, enter a Wallet label and click Create wallet. The wallet's page opens with its address and balance.

With your own account, keep or rename the Credential label, and enter your Privy app ID and Privy app secret. Under Wallet details, enter a Wallet label. Click Connect and create wallet: SDP checks the credentials with Privy, then creates the wallet, which appears on Wallets.

Privy provider setup with example credentials and a masked secret

The first wallet in a provider account is a transfer wallet.

5. Create more wallets

On Wallets, click Create a wallet and select the provider if asked. The account question lists SDP's account where it's offered, each of your connections, and Connect another Privy account. A connection that isn't ready shows what it needs, with a link to fix it.

For SDP's account or an existing connection, enter a Wallet label, pick a Purpose (it starts on Transfers), and click Create wallet. Connect another Privy account works as in step 4.

A connection signs only with its current credentials. If you rotate or remove them while a request is signing, that request returns 409 CONFLICT with details.reason set to runtime_execution_unavailable. Retry it as Idempotency describes for that operation. A transfer that recorded the failure replays it under the same Idempotency-Key, so send the transfer again with a new key.

1. Set up the provider in the dashboard

Setting up a custody provider takes wallets.configure, and no API key can hold a configure action. An organization admin sets it up once per project, signed in to the dashboard (see the Dashboard tab), which calls POST /v1/wallets/initialize with that session. The same call with an API key returns 403 INSUFFICIENT_PERMISSIONS with details.action set to wallets.configure.

Production projects take only your own provider account. POST /v1/wallets/initialize sets up SDP's account of the provider (Managed custody), so in a Production project it returns 403 with details.reason set to custody_mode_not_allowed. Connect your own account to the project in the dashboard instead. Then create wallets in that connection with connectionId, using an API key that holds wallets.manage:

Terminal
export IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST https://api.solana.com/v1/wallets \
  -H "Authorization: Bearer sf_..." \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "cconn_123",
    "label": "Treasury wallet"
  }'

2. Create more wallets

Every wallet names the provider account it lives under: provider for SDP's account of that provider (Managed), or connectionId for a connection your project added. Send exactly one of them; a body with both or neither returns 400. There is no default account to fall back to.

IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/wallets' \
  -H "Authorization: Bearer $SDP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "privy",
    "label": "Treasury wallet",
    "purpose": "transfer"
  }'
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SDP_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "provider": "privy",
    "label": "Treasury wallet",
    "purpose": "transfer"
  })
});
CreateWallet.java
String idempotencyKey = java.util.UUID.randomUUID().toString();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets"))
    .header("Authorization", "Bearer sf_...")
    .header("Idempotency-Key", idempotencyKey)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "provider": "privy",
          "label": "Treasury wallet",
          "purpose": "transfer"
        }"""))
    .build();

purpose is optional and defaults to transfer. Only a transfer wallet can be the source of a transfer, transfer batch, or recurring payment; any other purpose is refused with 403 and details.reason set to wallet_purpose. Reuse the same Idempotency-Key when you retry a wallet creation.

3. List wallets

Terminal
curl "https://api.solana.com/v1/wallets?view=summary" \
  -H "Authorization: Bearer sf_..."
list-wallets.ts
const response = await fetch("https://api.solana.com/v1/wallets?view=summary", {
  headers: { "Authorization": "Bearer sf_..." },
});
const { data } = await response.json();
// data.wallets includes id, walletId, publicKey, provider, label, purpose, and status
ListWallets.java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets?view=summary"))
    .header("Authorization", "Bearer sf_...")
    .GET()
    .build();

Every operation that signs names its wallet; there is no default wallet. Transfers take the wallet's id (cwlt_…) as sourceCustodyWalletId, and other modules' pages name their field. An API key limited to selected wallets lists the same id in walletIds.

transfer-request.ts
const body = {
  sourceCustodyWalletId: "cwlt_123", // `id` from GET /v1/wallets
  destination: "7xKXz...9fGh",
  token: "9aBCd...4eEf",
  amount: "100.00",
};