# Solana Developer Platform

> Full public documentation for Solana Developer Platform — all page content for AI and agent ingestion.

## Canonical URLs
- Docs: https://platform.solana.com/docs
- API: https://api.solana.com
- Interactive API docs: https://api.solana.com/docs
- OpenAPI: https://api.solana.com/openapi.json
- AI guide: https://platform.solana.com/docs/reference/ai-consumption

## Docs

### Solana Developer Platform Docs
Source: https://platform.solana.com/docs/home

> A dashboard and REST API for real-world asset issuance, payments, and markets on Solana — with built-in compliance controls.

<DocsHome />

---

### Introduction
Source: https://platform.solana.com/docs/introduction

> What SDP is, what it provides, and how it works.

Solana Developer Platform (SDP) is a dashboard and REST API for real-world asset (RWA) issuance, payments, and markets on Solana. It handles wallet custody, onchain transaction construction, and compliance operations so your team can bring regulated assets onchain without managing keys or setting up Solana infrastructure.

## What you get

**Token lifecycle management** — Create tokens from pre-built templates (stablecoin, tokenized security) or configure your own using Token-2022 extensions. Deploy, mint, burn, and manage supply through the dashboard or API.

**Wallet custody** — Provision signing wallets through integrated custody providers. SDP manages the signing infrastructure so your team never handles private keys directly.

**Projects and scoped access** — Organize integrations by project, assign members, and issue project-scoped API keys so teams or environments do not share the same wallet and token surface.

**Compliance operations** — Freeze individual accounts, pause token activity, seize tokens, and manage allowlists for regulated issuance flows. Selected compliance actions accept reason or memo fields for audit trails.

**Two signing modes** — SDP supports both modes for transaction-building flows such as issuance, payments, and compliance actions:

- **Execute** — SDP builds, signs, and submits the transaction in one API call
- **Prepare** — SDP builds the transaction and returns it unsigned. You sign with your own infrastructure (hardware wallet, multisig, internal HSM) and submit it yourself.

**Transaction simulation** — Prepare endpoints accept a `simulate: true` option that dry-runs the transaction before you sign, returning compute units consumed, program logs, and any errors.

**Scoped API keys** — Generate keys with specific permission sets (`tokens:read`, `tokens:write`, `tokens:admin`, etc.) and rotate them without downtime.

## Architecture

SDP exposes two interfaces that connect to the same backend:

- **Dashboard** — browser-based UI for setup, token management, and compliance operations. Sign in with email, Google, or GitHub.
- **REST API** — programmatic access for backend integrations. Authenticate with an API key in the `Authorization` header.

## Environments

| Environment | API key prefix | Solana network | Purpose                                     |
| ----------- | -------------- | -------------- | ------------------------------------------- |
| Sandbox     | `sk_test_`     | devnet         | Development and testing with no real assets |
| Production  | `sk_live_`     | mainnet-beta   | Live operations with real assets            |

Both environments expose identical APIs. Develop and test against sandbox, then switch to production by swapping your API key.

---

## Getting Started

### Set Up Your Organization
Source: https://platform.solana.com/docs/guides/setup-organization

> Create and configure your SDP organization through the dashboard.

An organization is the top-level container for all your SDP resources — projects, wallets, API keys, and tokens. Every API request is scoped to an organization.

### 1. Sign up

Go to [platform.solana.com](https://platform.solana.com) and click **Dashboard**. On the sign-in screen, click **Sign up** to create a new account.

![SDP sign-in screen with Sign up link](/images/getting-started/sign-in.png)

Enter your email address and a password to create your account.

![Create your account screen](/images/getting-started/create-account.png)

### 2. Create or select an organization

After signing in you'll be prompted to set up your organization. Upload a logo and enter your organization name, then click **Continue**.

![Setup your organization screen](/images/getting-started/setup-organization.png)

If you already have an organization, the **Organization Switcher** in the top-left of the sidebar lets you switch between them or create a new one.

<img src="/images/getting-started/org-switcher.png" alt="Organization Switcher dropdown" width="436" style={{ display: "block", maxWidth: "100%" }} />

### 3. Next steps

- [Set up wallets](/docs/guides/setup-wallets) for signing transactions
- [Create API keys](/docs/guides/manage-api-keys) for programmatic access
- [Projects API reference](/docs/reference/api/projects) when you want to segment apps, tenants, or environments within the organization
- [Create your first token](/docs/guides/create-a-token)

---

### Set Up Wallets
Source: https://platform.solana.com/docs/guides/setup-wallets

> Initialize a custody provider and create wallets for signing transactions.

Wallets are Solana keypairs managed by a custody provider. SDP uses them to sign transactions for token deployment, minting, transfers, and other onchain operations. You must initialize a signing provider before creating tokens or executing transactions.

## Supported providers

| Provider | Description |
| --- | --- |
| **Privy** | Embedded wallet infrastructure platform |
| **Fireblocks** | Digital asset infrastructure company |
| **Coinbase CDP** | Embedded wallets for developers |
| **Para** | Wallet and authentication suite |
| **Turnkey** | Non-custodial wallet infrastructure platform |
| **DFNS** | Digital asset wallet infrastructure |
| **Anchorage** | Regulated institutional crypto custody |

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

### 1. Navigate to Wallets

Open the sidebar and click **Wallets**. If you haven't linked your organization yet, complete the [organization setup](/docs/guides/setup-organization) first.

### 2. Choose a custody provider

The Wallets page shows all supported providers, each with a **New wallet** button. Pick the provider that fits your setup.

![Wallets page showing custody provider cards](/images/getting-started/wallet-providers.png)

### 3. Create the wallet

Click **New wallet** on your chosen provider. In the modal, enter a label for the wallet and click **Create wallet**.

<img src="/images/getting-started/wallet-create-modal.png" alt="New wallet modal" width="696" style={{ display: "block", maxWidth: "100%" }} />

### 4. View your wallet

After provisioning, the wallet card appears on the Wallets page with its address, wallet ID, and balance.

![Wallets page after wallet creation](/images/getting-started/wallet-created.png)

To create additional wallets, click **Create Wallet** in the top right and repeat the process.

### 5. Provider capabilities

Each provider supports a different set of operations shown as capability chips:

- **Issuance** — can deploy and mint tokens
- **Transfers** — can sign payment transactions
- **Compliance** — can sign freeze/unfreeze instructions

</Tab>
<Tab value="API">

#### 1. Initialize the signing provider

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/wallets/initialize \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "privy",
    "walletLabel": "Master wallet"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch("https://api.solana.com/v1/wallets/initialize", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    provider: "privy",
    walletLabel: "Master wallet",
  }),
});
const { data } = await response.json();
// data: { configId, publicKey, walletId }
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets/initialize"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "provider": "privy",
          "walletLabel": "Master wallet"
        }"""))
    .build();
```
</Tab>
</Tabs>

#### 2. Create additional wallets

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/wallets \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Mint authority wallet",
    "purpose": "mint_authority",
    "setDefault": false
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch("https://api.solana.com/v1/wallets", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    label: "Mint authority wallet",
    purpose: "mint_authority",
    setDefault: false,
  }),
});
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "label": "Mint authority wallet",
          "purpose": "mint_authority",
          "setDefault": false
        }"""))
    .build();
```
</Tab>
</Tabs>

The `purpose` field is optional metadata in the API. The current dashboard create-wallet flow does not expose wallet-purpose selection.

#### 3. List wallets

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl "https://api.solana.com/v1/wallets?view=summary" \
  -H "Authorization: Bearer sk_test_..."
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch("https://api.solana.com/v1/wallets?view=summary", {
  headers: { "Authorization": "Bearer sk_test_..." },
});
const { data } = await response.json();
// data.wallets includes walletId, publicKey, provider, label, purpose, and status
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets?view=summary"))
    .header("Authorization", "Bearer sk_test_...")
    .GET()
    .build();
```
</Tab>
</Tabs>

Use the returned `walletId` when setting a default wallet or binding wallet-scoped API keys.

#### 4. Set a default wallet

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/wallets/default-wallet \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "walletId": "wal_xyz789" }'
```
</Tab>
<Tab value="TypeScript">
```typescript
await fetch("https://api.solana.com/v1/wallets/default-wallet", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ walletId: "wal_xyz789" }),
});
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/wallets/default-wallet"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        { "walletId": "wal_xyz789" }"""))
    .build();
```
</Tab>
</Tabs>

</Tab>
</Tabs>

---

### Manage API Keys
Source: https://platform.solana.com/docs/guides/manage-api-keys

> Create, rotate, and revoke API keys with role-based access and environment scoping.

API keys authenticate every request to the SDP API. Each key is scoped to an environment (sandbox or production) and assigned a role that determines what operations it can perform.

## Key format

| Environment | Prefix | Solana network |
| --- | --- | --- |
| Sandbox | `sk_test_` | devnet |
| Production | `sk_live_` | mainnet-beta |

## Roles

| Role | Description |
| --- | --- |
| `api_admin` | Full access including custody and platform operations |
| `api_developer` | Read/write access, excludes custody actions |
| `api_readonly` | Read-only access to all resources |

## Create a key

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

Navigate to **API keys** in the sidebar. You'll start with an empty list.

![API keys page with no keys yet](/images/getting-started/api-keys-empty.png)

Click **New API key** in the top right. Fill in the key details:

- **Name** — a descriptive label (e.g., "CI deploy key")
- **Role** — Admin, Developer, or Read only
- **Environment** — Sandbox or Production
- **Wallet access** — All wallets or Selected wallets
- **Expiration (optional)** — date/time picker

![Create API key modal](/images/getting-started/api-key-create.png)

Click **Continue**. Review the summary and click **Create key**.

![Review API key modal](/images/getting-started/api-key-review.png)

The full key appears once in the **API key generated** modal. Click **Copy** and save it — it will not be shown again.

![API key generated modal showing the full key](/images/getting-started/api-key-generated.png)

After dismissing, the key appears in the table with its prefix, role, environment, and status.

![API keys list with one active key](/images/getting-started/api-keys-list.png)

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/api-keys \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI deploy key",
    "role": "api_developer",
    "environment": "sandbox",
    "walletScope": "all",
    "expiresAt": "2026-12-31T23:59:59Z"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch("https://api.solana.com/v1/api-keys", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "CI deploy key",
    role: "api_developer",
    environment: "sandbox",
    walletScope: "all",
    expiresAt: "2026-12-31T23:59:59Z",
  }),
});
const { data } = await response.json();
// data.apiKey.key — save this, only shown once
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "name": "CI deploy key",
          "role": "api_developer",
          "environment": "sandbox",
          "walletScope": "all",
          "expiresAt": "2026-12-31T23:59:59Z"
        }"""))
    .build();
```
</Tab>
</Tabs>

The response includes the full `key` value — **save it, it is only returned once**.

Optional fields: `allowedIps` (CIDR ranges), `permissions` (fine-grained), `signingWalletId`, `walletBindings`.

</Tab>
</Tabs>

## Rotate a key

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

In the API keys table, open the **Actions** dropdown next to the key and click **Rotate key (24h grace)**. The dashboard always uses a 24-hour grace period — use the API if you need a custom value (0–168h).

![Actions dropdown showing Rotate key and Delete key options](/images/getting-started/api-key-rotate.png)

During the grace period both the old and new key are valid. The new key value appears once in the generated key modal — save it immediately.

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/api-keys/key_abc123/rotate \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "gracePeriodHours": 24 }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/api-keys/key_abc123/rotate",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ gracePeriodHours: 24 }),
  }
);
const { data } = await response.json();
// data.apiKey — the new key
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123/rotate"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        { "gracePeriodHours": 24 }"""))
    .build();
```
</Tab>
</Tabs>

The old key remains valid for the grace period (0–168 hours).

</Tab>
</Tabs>

## Revoke a key

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

Open the **Actions** dropdown next to the key and click **Delete key**. The key stops working immediately and cannot be restored.

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X DELETE https://api.solana.com/v1/api-keys/key_abc123 \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "confirmation": "CI deploy key" }'
```
</Tab>
<Tab value="TypeScript">
```typescript
await fetch("https://api.solana.com/v1/api-keys/key_abc123", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ confirmation: "CI deploy key" }),
});
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .method("DELETE", HttpRequest.BodyPublishers.ofString("""
        { "confirmation": "CI deploy key" }"""))
    .build();
```
</Tab>
</Tabs>

The `confirmation` field must match the key's **name**. The key stops working immediately.

</Tab>
</Tabs>

---

## Tokens

### Tokenize an Asset
Source: https://platform.solana.com/docs/tokens/tokenize-an-asset

> Map a real-world asset to the right SDP token model, authorities, and implementation flow.

Use this page when you want to issue a stablecoin, tokenized security, loyalty token, or any other asset through Solana Developer Platform. It walks you from business decision to the first API call.

## Choose a token model

Pick the template that matches your asset's compliance and operational requirements:

| Asset model | Template | Typical controls |
| --- | --- | --- |
| Fiat-backed stablecoin | `stablecoin` | Mint authority, freeze authority, pausable, permanent delegate |
| Tokenized security or RWA | `tokenized-security` | Allowlist required, freeze authority, pausable, permanent delegate |
| Loyalty, rewards, or utility token | `custom` | Manually selected extensions |
| Non-standard asset | `custom` | Manually selected extensions |

Use SDP-controlled custody wallets for your main authorities when you expect SDP execute flows to work without external signing.

## Implementation sequence

Each step links to its own guide with UI and API instructions:

1. [Set Up Your Organization](/docs/guides/setup-organization)
2. [Set Up Wallets](/docs/guides/setup-wallets)
3. [Manage API Keys](/docs/guides/manage-api-keys)
4. [Create a Token](/docs/tokens/create-a-token)
5. [Deploy a Token](/docs/tokens/deploy-a-token)
6. [Mint and Burn](/docs/tokens/mint-and-burn)
7. [Basic payment](/docs/payments/send-basic-payment) — transfer tokens between wallets
8. [Allowlists](/docs/tokens/allowlists) — required for `tokenized-security`
9. [Manage Token Settings](/docs/tokens/manage-token-settings) — freeze, pause, seize for compliance workflows

## Core API endpoints

| Method | Endpoint | Purpose |
| --- | --- | --- |
| `GET` | `/v1/issuance/templates` | List available token templates |
| `POST` | `/v1/issuance/tokens` | Create a token definition |
| `POST` | `/v1/issuance/tokens/{tokenId}/deploy` | Deploy to Solana |
| `POST` | `/v1/issuance/tokens/{tokenId}/mint` | Mint new supply |
| `POST` | `/v1/issuance/tokens/{tokenId}/burn` | Reduce supply |
| `POST` | `/v1/issuance/tokens/{tokenId}/allowlist` | Add an allowed address |
| `POST` | `/v1/issuance/tokens/{tokenId}/freeze` | Freeze an account |
| `POST` | `/v1/issuance/tokens/{tokenId}/pause` | Pause all transfers |
| `POST` | `/v1/payments/transfers` | Transfer tokens |

See the full schema in the [Issuance API reference](/docs/reference/api/issuance) and [Payments API reference](/docs/reference/api/payments).

## Operational constraints

- A token must be **created** before deployment, and **deployed** before minting or transfer.
- If the token is paused, minting and transfer operations are unavailable until it is unpaused.
- Freeze and unfreeze flows accept a holder wallet address; SDP derives the associated token account automatically.
- If a relevant authority is held outside SDP, prefer prepare flows or rotate the authority to a controlled wallet first.

## For AI agents

If you are driving SDP through an agent workflow, start from [AI Consumption](/docs/reference/ai-consumption). That page links the machine-readable SDP discovery files and keeps AI-facing guidance grounded in the supported public API surface.

---

### Create a Token
Source: https://platform.solana.com/docs/tokens/create-a-token

> Define a new token using a template or custom configuration.

Token creation defines your token's metadata, extensions, and operational rules. The token is saved in SDP but **not yet deployed** onchain — see [Deploy a Token](/docs/tokens/deploy-a-token) for the next step.

![Empty Issuance page with a create card](/images/tokens/token-list-empty.png)

## Templates

| Template | Decimals | Key extensions | Use case |
| --- | --- | --- | --- |
| `stablecoin` | 6 | Permanent delegate, pausable | USD-backed tokens |
| `tokenized-security` | 8 | Permanent delegate, pausable, scaled UI amount, allowlist required | Regulated assets |
| `custom` | 9 | None — you choose | Full control |

## How it works

<HowItWorks>

<Step number={1} title="Choose a template">

Select the template that matches your asset model. Templates pre-fill extensions and constraints you can review before confirming.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Navigate to **Issuance** in the sidebar and click **Create token** in the top right corner.

The first screen shows three template cards:

- **Stablecoin** — USD-backed tokens with compliance controls
- **Tokenized Security** — regulated assets with allowlist requirement
- **Custom** — fully configurable Token-2022 setup

Select a template and click **Continue**.

![Template selection modal showing Stablecoin, Tokenized Security, and Custom cards](/images/tokens/token-create-templates.png)

</Tab>
<Tab value="API">

List available templates to see what fields each one pre-configures:

```bash
curl https://api.solana.com/v1/issuance/templates \
  -H "Authorization: Bearer sk_test_..."
```

```typescript
const res = await fetch("https://api.solana.com/v1/issuance/templates", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const { data } = await res.json();
// data[].id — use as the "template" value when creating
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Configure token identity">

Set the core metadata: name, symbol, decimals, and a URI pointing to your token metadata JSON.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In step 2 of 3, fill in the identity fields:

- **Metadata URI** — HTTPS URL for the token metadata JSON
- **Token name** — e.g., "Acme Dollar"
- **Symbol** — 1–10 uppercase alphanumeric characters, e.g., `ACME`
- **Decimals** — options depend on the selected template

Click **Continue** when ready.

![Identity configuration form with Metadata URI, Token Name, Symbol, and Decimals fields](/images/tokens/token-create-identity.png)

</Tab>
<Tab value="API">

Pass identity fields in the `POST /v1/issuance/tokens` body:

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Dollar",
    "symbol": "ACME",
    "decimals": 6,
    "description": "USD-backed stablecoin",
    "template": "stablecoin"
  }'
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Set operational settings">

Choose the wallet that will sign token operations, and configure allowlist and supply controls.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In step 3 of 3, configure:

- **Main signer** — the SDP-controlled wallet for deploy and token actions
- **Allowlist** — toggle on or off; required and locked for `tokenized-security`

Click **Create token** to save the draft.

![Operational settings form showing Main Signer and Transfer Controls with Allowlist and Denylist toggles](/images/tokens/token-create-settings.png)

</Tab>
<Tab value="API">

Add operational fields to the same body:

```typescript
const res = await fetch("https://api.solana.com/v1/issuance/tokens", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Acme Dollar",
    symbol: "ACME",
    decimals: 6,
    description: "USD-backed stablecoin",
    template: "stablecoin",
    requiresAllowlist: false,
    isMintable: true,
    isFreezable: true,
  }),
});
```

For a custom token, omit `template` or set it to `"custom"` and pass `overrides.extensions`:

```typescript
body: JSON.stringify({
  name: "Game Coin",
  symbol: "GAME",
  decimals: 0,
  template: "custom",
  overrides: {
    extensions: {
      pausable: {},
      transferFee: { basisPoints: 50, maxFee: "1000" },
    },
  },
  isMintable: true,
  isFreezable: false,
}),
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={4} title="Confirm creation">

The token is saved in SDP with status `pending`. Nothing has been written to Solana yet.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

A success toast confirms creation. The token appears in the **Issuance** list with a `Draft` badge.

![Issuance list showing the newly created token with a Draft badge](/images/tokens/token-list-draft.png)

Open the token to see its detail page. The status shows **Not deployed** and a **Deploy** button is available to move to the next step.

![Token detail page showing Not deployed status and Deploy button](/images/tokens/token-detail-pending.png)

</Tab>
<Tab value="API">

A successful response returns the new token object:

```typescript
const { data } = await res.json();
// data.token.id     — save this; used in all subsequent calls
// data.token.status — "pending"
```

The token is not yet onchain. Pass `data.token.id` to [Deploy a Token](/docs/tokens/deploy-a-token).

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

---

### Deploy a Token
Source: https://platform.solana.com/docs/tokens/deploy-a-token

> Deploy a created token to the Solana blockchain.

After [creating a token](/docs/tokens/create-a-token), deploy it onchain. Deployment creates the mint account on Solana and assigns authorities to your configured wallets.

## Prerequisites

- A token in `pending` status
- A [wallet configured](/docs/guides/setup-wallets) as the token's main signer

## How it works

<HowItWorks>

<Step number={1} title="Open the deploy action">

Locate the token you want to deploy. Deployment is irreversible — the mint account is created onchain and authorities are locked to your configured wallets.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Issuance** list, click the token with a `Pending` badge to open its detail page.

The **Deploy** button appears in the token header and in the fund management panel.

![Token detail page showing Not deployed status and Deploy button](/images/tokens/token-detail-pending.png)

</Tab>
<Tab value="API">

You need the `tokenId` returned when you created the token. If you don't have it, list your tokens:

```bash
curl https://api.solana.com/v1/issuance/tokens \
  -H "Authorization: Bearer sk_test_..."
```

Note the `id` field (e.g., `tok_abc123`) — required for all deploy and post-deploy calls.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Deploy the token">

Use **execute mode** when SDP controls the signing wallet, or **prepare mode** when you need to sign externally.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Click **Deploy**. A modal shows the signer wallet that will authorize the transaction. Review the wallet ID and public key, then click **Deploy now**.

![Deploy token modal showing the signer wallet details](/images/tokens/token-deploy-modal.png)

A confirmation prompt asks you to confirm the on-chain submission. Click **Deploy now** to proceed.

![Deploy token confirmation dialog](/images/tokens/token-deploy-confirm.png)

SDP submits the transaction to Solana and polls for confirmation. The page updates automatically when deployment completes.

</Tab>
<Tab value="API">

**Execute mode** — SDP signs and submits:

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/deploy \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: deploy-acme-001"
```

```typescript
const res = await fetch(
  "https://api.solana.com/v1/issuance/tokens/tok_abc123/deploy",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_test_...",
      "Idempotency-Key": "deploy-acme-001",
    },
  }
);
const { data } = await res.json();
// data.token.mintAddress — the onchain mint address
```

**Prepare mode** — returns an unsigned transaction:

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/deploy/prepare \
  -H "Authorization: Bearer sk_test_..."
```

Sign `data.transaction.serialized` with your private key and submit to Solana. See [Prepare vs Execute](/docs/tokens/prepare-vs-execute).

Always pass an `Idempotency-Key` — retrying with the same key after a network error prevents double-deployment.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Confirm deployment">

Once the transaction is confirmed, the token is active and ready for minting and transfers.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

The token status badge changes from `Not deployed` to the onchain mint address. The detail page shows Mint and Burn actions in the Operations tab, and a confirmed deploy transaction in the Transactions table.

![Deployed token detail page with mint address, Operations tab, and confirmed deploy transaction](/images/tokens/token-deployed.png)

</Tab>
<Tab value="API">

The deploy response includes the confirmed state:

```typescript
// data.token.status      — "active"
// data.token.mintAddress — e.g., "AcMeXYZ..."
```

Fetch the token at any time to verify:

```bash
curl https://api.solana.com/v1/issuance/tokens/tok_abc123 \
  -H "Authorization: Bearer sk_test_..."
```

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

---

### Mint and Burn
Source: https://platform.solana.com/docs/tokens/mint-and-burn

> Increase or decrease token supply by minting to an address or burning from an account you control.

After [deploying a token](/docs/tokens/deploy-a-token), you can mint new tokens to any address and burn tokens from accounts you control. Force-burn lets admins burn from any account without the holder's signature.

## Prerequisites

- A deployed token with status `active`
- `tokens:write` permission — or `tokens:admin` for force-burn

## How it works

<HowItWorks>

<Step number={1} title="Select the token and action">

Open the token you want to manage. Mint and burn are both available from the token detail page.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Issuance** list, click an active token to open its detail page.

The **Fund management** panel shows the current supply with two primary actions: **Mint** and **Burn**.

![Token Operations tab showing Mint and Burn actions](/images/tokens/token-deployed.png)

</Tab>
<Tab value="API">

All supply operations share this URL pattern:

```
POST /v1/issuance/tokens/{tokenId}/mint
POST /v1/issuance/tokens/{tokenId}/burn
POST /v1/issuance/tokens/{tokenId}/force-burn
```

The `amount` field is a decimal string in **UI units** (human-readable token amounts). `"1"` means one token; `"1.5"` means one and a half. SDP converts to on-chain base units using the token's `decimals`.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Mint tokens">

Issue new tokens to a destination address. SDP derives the associated token account automatically.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Mint** form:

- **Destination** — wallet address to receive the new tokens
- **Amount** — in human-readable units (e.g., `1.0`)
- **Memo** — optional, recorded onchain

Click **Mint**. A modal shows the signer wallet and a destination picker — select or paste the destination address.

Fill in the amount and optional memo, then click **Mint tokens**.

![Mint Tokens modal with amount and memo filled in](/images/tokens/mint-modal-filled.png)

Confirm the on-chain submission in the confirmation prompt.

![Mint tokens confirmation dialog — click Mint now to submit the transaction on-chain](/images/tokens/mint-confirm.png)

Once confirmed, the mint appears in the Transactions table with a `confirmed` status and a "Mint transaction finalized." toast appears.

![Operations tab showing confirmed mint entry and Mint transaction finalized toast](/images/tokens/mint-confirmed.png)

</Tab>
<Tab value="API">

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/mint \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: mint-001" \
  -d '{
    "mint": {
      "destination": "7xKXz...9fGh",
      "amount": "1000",
      "memo": "Initial distribution"
    }
  }'
```

```typescript
const res = await fetch(
  "https://api.solana.com/v1/issuance/tokens/tok_abc123/mint",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_test_...",
      "Content-Type": "application/json",
      "Idempotency-Key": "mint-001",
    },
    body: JSON.stringify({
      mint: {
        destination: "7xKXz...9fGh",
        amount: "1000",
        memo: "Initial distribution",
      },
    }),
  }
);
```

Add `/prepare` to the path to simulate before signing. Control priority with `options.priorityFee`: `"none"`, `"low"`, `"medium"`, `"high"`, or a micro-lamport value.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Burn tokens">

Reduce supply by burning from an account your wallet controls. Use force-burn to burn from any holder account without their signature.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Click **Burn**. A modal shows the signer and a source field — select the wallet holding the tokens you want to remove.

![Burn Tokens modal with empty source field](/images/tokens/burn-modal-empty.png)

Select the source wallet from the dropdown.

![Burn Tokens modal with source wallet dropdown open](/images/tokens/burn-modal-source.png)

Fill in the amount and optional memo, then click **Burn tokens**.

![Burn Tokens modal filled with source, amount, and memo](/images/tokens/burn-modal-filled.png)

Confirm the on-chain submission in the confirmation prompt.

![Burn tokens confirmation dialog](/images/tokens/burn-confirm.png)

The "Burn transaction finalized." toast appears and the burn shows as confirmed in the Transactions table.

![Operations tab showing confirmed burn transaction and finalized toast](/images/tokens/burn-confirmed.png)

For **Force burn** (requires `tokens:admin`), use the Compliance tab instead of the Operations tab — force-burn lets admins remove tokens from any holder account without their signature.

</Tab>
<Tab value="API">

**Burn from a controlled account:**

```typescript
await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/burn", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Content-Type": "application/json",
    "Idempotency-Key": "burn-001",
  },
  body: JSON.stringify({
    burn: { source: "3xYZa...2aBc", amount: "250", memo: "Redemption" },
  }),
});
```

**Force-burn from any holder** (requires `tokens:admin`):

```typescript
await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/force-burn", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Content-Type": "application/json",
    "Idempotency-Key": "force-burn-001",
  },
  body: JSON.stringify({
    forceBurn: { source: "3xYZa...2aBc", amount: "250", memo: "Compliance action" },
  }),
});
```

Both endpoints support `/prepare` for client-side signing.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={4} title="Refresh supply">

After minting or burning, refresh the cached supply total.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

The dashboard refreshes supply automatically after a confirmed operation. The Transactions table shows all completed operations — deploy, mint, and burn — each with a `confirmed` status.

![Operations tab showing confirmed burn, mint, and deploy transactions](/images/tokens/operations-history.png)

Reload the token detail page if the displayed supply value appears stale.

</Tab>
<Tab value="API">

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/supply/refresh \
  -H "Authorization: Bearer sk_test_..."
```

Safe to include in automated post-mint workflows.

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

---

### Allowlists
Source: https://platform.solana.com/docs/tokens/allowlists

> Control which addresses can receive your token by maintaining an approved list.

Tokens created with `requiresAllowlist: true` enforce a list of approved destination addresses on allowlist-aware issuance flows such as minting and administrative transfers. Required for `tokenized-security` tokens. Payment transfers use wallet-level destination allowlists instead.

## Prerequisites

- A token created with `requiresAllowlist` enabled
- `tokens:write` permission on the API key

## How it works

<HowItWorks>

<Step number={1} title="Enable the allowlist">

The allowlist is configured at token creation time. Decide before creating whether your token requires one — it cannot be changed after deployment.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Create token** flow, step 3 of 3, toggle **Allowlist** on.

For `tokenized-security` tokens, the allowlist toggle is pre-enabled and locked.

The Compliance tab on the token detail page shows the allowlist management interface, with an Address and Label field and a Control Lists sidebar tracking entry and frozen account counts.

![Operational settings form showing the Allowlist toggle during token creation](/images/tokens/token-create-settings.png)

</Tab>
<Tab value="API">

Set `requiresAllowlist: true` when creating the token:

```typescript
body: JSON.stringify({
  name: "Acme Security",
  symbol: "ACMES",
  decimals: 8,
  template: "tokenized-security",
  requiresAllowlist: true,
  isMintable: true,
  isFreezable: true,
}),
```

Once deployed, the allowlist is enforced automatically on issuance flows.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Add an address">

Add a wallet address to the allowlist before minting to it or using it as an administrative transfer destination.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Open the token detail page and go to the **Compliance** tab, then select **Allowlist**.

Enter the wallet address and an optional label, then click **Add allowlist entry**. The address is enforced on the next allowlist-checked operation.

![Compliance Allowlist tab with empty address and label fields, ready to add a new entry](/images/tokens/allowlist-empty.png)

After adding, the entry appears below the form with a **Remove entry** button.

![Allowlist entry added and visible in the list](/images/tokens/allowlist-entry-added.png)

</Tab>
<Tab value="API">

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "address": "7xKXz...9fGh",
    "label": "Treasury wallet"
  }'
```

```typescript
const res = await fetch(
  "https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ address: "7xKXz...9fGh", label: "Treasury wallet" }),
  }
);
// res.data.entry.id — save this to remove the entry later
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Review the list">

Check which addresses are currently approved. Useful before minting to a new destination or during a compliance audit.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

The **Allowlist** tab shows all approved addresses, their labels, and when they were added.

Use the search field to find a specific address by label or public key.

</Tab>
<Tab value="API">

```bash
curl "https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist?search=So1&label=Treasury" \
  -H "Authorization: Bearer sk_test_..."
```

```typescript
const { data, meta } = await res.json();
// meta: { total, page, pageSize, hasMore }
```

Query parameters:

- `page`, `pageSize` — pagination (`pageSize` max 500, default 50).
- `search` — contains-style match over the entry address **and** label. A blank value is treated as no filter.
- `label` — restrict to entries with this exact label.

Search and the label filter run server-side over the whole list, so results aren't limited to the current page.

To populate a label filter, fetch the distinct labels in use (the response also
includes `total`, the unfiltered entry count, for a summary display):

```bash
curl https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist/labels \
  -H "Authorization: Bearer sk_test_..."
```

```typescript
const { data } = await res.json();
// data: { labels: ["Market maker", "Treasury"], total: 42 }
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={4} title="Remove an address">

Remove an address when a holder's compliance status changes. Existing token holdings are unaffected — only future allowlist-enforced flows are blocked.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Compliance → Allowlist** tab, click **Remove entry** next to the address. The entry is removed immediately and the Control Lists sidebar updates to 0 entries.

</Tab>
<Tab value="API">

```bash
curl -X DELETE \
  https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist/alw_abc123 \
  -H "Authorization: Bearer sk_test_..."
```

Use the `entry.id` returned when the address was added.

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

## Typical workflow for a tokenized security

1. Create the token with `requiresAllowlist: true` and template `tokenized-security`
2. [Deploy the token](/docs/tokens/deploy-a-token)
3. Collect wallet addresses from KYC-verified investors
4. Add each verified address to the allowlist
5. [Mint tokens](/docs/tokens/mint-and-burn) to allowlisted addresses
6. Remove addresses when compliance status changes

---

### Freeze and Compliance
Source: https://platform.solana.com/docs/tokens/freeze-and-compliance

> Screen addresses, freeze accounts, pause transfers, and seize tokens for compliance workflows.

SDP provides two categories of compliance tooling:

- **Token controls** — freeze individual accounts, pause all transfers, or seize tokens. Available for tokens with `isFreezable` enabled or the `pausable` extension.
- **Address screening** — check a wallet against configured compliance providers before approving a destination or allowlisting it.

Token-control actions require `tokens:admin` permission on the API key.

## How it works

<HowItWorks>

<Step number={1} title="Screen an address">

Check a Solana address against configured compliance providers before onboarding a wallet or approving it as a transfer destination.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Use the **Screen address** action in the Compliance section of the dashboard.

Enter the wallet address and select the intent (e.g., transfer destination or allowlist candidate). Results appear per provider — each showing risk level, score, and flags.

</Tab>
<Tab value="API">

```bash
curl -X POST https://api.solana.com/v1/compliance/address-screenings \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "address": "8dHEsGLp...hQWRGZ",
    "network": "solana",
    "intent": "transfer_destination"
  }'
```

```typescript
const { data } = await res.json();
// data.screening.providers[]
// → { provider, status, riskScore, riskLevel, message, evaluatedAt }
```

See the [Compliance API reference](/docs/reference/api/compliance) for the full schema.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Freeze and unfreeze an account">

Prevent a specific holder from sending or receiving this token. The freeze targets one associated token account — other accounts are unaffected.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

On the token detail page, go to the **Compliance** tab and select **Freeze Controls**.

![Freeze Controls tab with empty wallet address and reason fields](/images/tokens/freeze-controls-empty.png)

Enter the holder's wallet address and an optional reason, then click **Freeze account**.

![Freeze Controls tab with wallet address and reason filled in](/images/tokens/freeze-controls-filled.png)

Confirm the on-chain submission in the confirmation prompt.

![Freeze account confirmation dialog](/images/tokens/freeze-confirm.png)

The "Freeze transaction finalized." toast appears and the Frozen Accounts count updates to 1.

![Compliance tab showing Freeze transaction finalized toast and Frozen Accounts count updated to 1](/images/tokens/freeze-confirmed.png)

To lift the freeze, find the account in the frozen list and click **Unfreeze**. Confirm the on-chain submission.

![Unfreeze account confirmation dialog](/images/tokens/unfreeze-confirm.png)

The "Unfreeze transaction finalized." toast appears and the Frozen Accounts count returns to 0.

![Compliance tab showing Unfreeze transaction finalized toast and Frozen Accounts count back to 0](/images/tokens/unfreeze-confirmed.png)

</Tab>
<Tab value="API">

**Freeze:**

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/freeze \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "accountAddress": "3xYZa...2aBc",
    "reason": "Suspicious activity investigation"
  }'
```

`accountAddress` accepts a holder wallet or its token account — SDP derives the associated token account when needed.

**Unfreeze:**

```typescript
await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/unfreeze", {
  method: "POST",
  headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json" },
  body: JSON.stringify({ accountAddress: "3xYZa...2aBc" }),
});
```

**List frozen accounts:**

```bash
curl https://api.solana.com/v1/issuance/tokens/tok_abc123/frozen \
  -H "Authorization: Bearer sk_test_..."
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Pause and unpause the token">

Halt all minting and transfer activity globally. Requires the `pausable` extension on the token.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

On the token detail page, go to the **Compliance** tab and select **Pause Controls**. Click **Pause token**.

![Pause Controls tab showing Pause token and Unpause token buttons](/images/tokens/pause-controls.png)

Confirm the on-chain submission in the confirmation prompt.

![Pause token confirmation dialog](/images/tokens/pause-confirm.png)

The "Pause transaction finalized." toast appears and an orange **Token is paused** banner displays across the page. All supply and transfer operations are now blocked.

![Compliance tab showing Token is paused banner and Pause transaction finalized toast](/images/tokens/pause-confirmed.png)

To resume, click **Unpause token** and confirm.

![Unpause token confirmation dialog](/images/tokens/unpause-confirm.png)

</Tab>
<Tab value="API">

```bash
# Pause
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/pause \
  -H "Authorization: Bearer sk_test_..."

# Unpause
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/unpause \
  -H "Authorization: Bearer sk_test_..."
```

While paused, all transfer-related and supply-management operations are rejected until unpause.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={4} title="Seize tokens">

Force-transfer tokens from a holder to a recovery address without the holder's signature. For court orders and regulatory enforcement. Requires `tokens:admin`.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In the **Compliance** tab, click **Seize tokens**. Provide the source, destination, amount, and a memo (strongly recommended for audit records). Confirm to submit.

</Tab>
<Tab value="API">

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/seize \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: seize-001" \
  -d '{
    "seize": {
      "source": "3xYZa...2aBc",
      "destination": "9aBCd...4eEf",
      "amount": "250",
      "memo": "Court order #12345"
    }
  }'
```

Seize also supports `/prepare` for client-side signing.

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

## Compliance response workflow

1. **Screen** the address via the Compliance API
2. **Freeze** the affected account while investigating
3. Based on findings — **Unfreeze** if cleared, **Seize** tokens to a recovery address, or **Force-burn** (see [Mint and Burn](/docs/tokens/mint-and-burn))
4. Record all actions with `memo` fields for the audit trail

---

### Manage Token Settings
Source: https://platform.solana.com/docs/tokens/manage-token-settings

> Update token authorities and operational settings after deployment.

After a token is deployed, you can rotate its authorities and adjust operational settings. Authority management is separate from compliance actions — for freeze, pause, and seize flows see [Freeze and Compliance](/docs/tokens/freeze-and-compliance).

## Prerequisites

- A deployed token with status `active`
- `tokens:admin` permission on the API key

## How it works

<HowItWorks>

<Step number={1} title="View current settings">

Review the token's current authorities and configuration before making changes.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Open the token detail page. The **Overview** tab shows the token address, mint authority, current supply, and core metadata.

![Token detail Overview tab showing token address, mint authority, supply, template, and decimals](/images/tokens/token-settings-overview.png)

The **Permissions** tab lists every authority and which wallet currently holds it.

![Permissions tab showing Mint Authority, Freeze Authority, Metadata Authority, and Permanent Delegate Authority with Edit buttons](/images/tokens/token-settings-permissions.png)

The **Extensions** tab shows the template and each operational flag — allowlist status, mintable, freezable, and default account state.

![Extensions tab showing template, allowlist, mintable, freezable, and default account state settings](/images/tokens/token-settings-extensions.png)

</Tab>
<Tab value="API">

Fetch the current token state:

```bash
curl https://api.solana.com/v1/issuance/tokens/tok_abc123 \
  -H "Authorization: Bearer sk_test_..."
```

```typescript
const { data } = await res.json();
// data.token.authorities — { mintAuthority, freezeAuthority, permanentDelegate }
// data.token.extensions  — active extensions on the token
// data.token.status      — "active" | "paused"
```

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Rotate an authority">

Transfer a token authority to a different wallet. Use this to move signing responsibilities or to hand off control to an external key holder.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

On the token detail page, go to **Permissions**. Click **Edit** next to the authority you want to change. Select another controlled wallet or choose **Use custom address**, then click **Save authority**.

![Mint Authority modal showing current authority wallet, new authority dropdown with controlled wallets, and Save authority button](/images/tokens/token-authority-edit.png)

> Once an authority is rotated to an external wallet, SDP can no longer sign on its behalf. Use execute flows with the external wallet or prepare flows for signing outside SDP.

</Tab>
<Tab value="API">

**Execute mode** — SDP signs the authority rotation:

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/authority \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rotate-001" \
  -d '{
    "authority": {
      "type": "mintAuthority",
      "newAuthority": "9aBCd...4eEf"
    }
  }'
```

**Prepare mode** — returns unsigned transaction for external signing:

```bash
curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/authority/prepare \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "authority": {
      "type": "mintAuthority",
      "newAuthority": "9aBCd...4eEf"
    }
  }'
```

`type` can be `mintAuthority`, `freezeAuthority`, or `permanentDelegate`.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Revoke an authority">

Permanently remove an authority from the token. This action is irreversible — the token will no longer be mintable, freezable, or delegatable depending on which authority is revoked.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

In **Settings → Authorities**, click **Revoke** next to the authority and confirm. A warning dialog explains the consequences before you proceed.

</Tab>
<Tab value="API">

Set `newAuthority` to `null` to revoke:

```typescript
body: JSON.stringify({
  authority: {
    type: "mintAuthority",
    newAuthority: null,
  },
}),
```

After revoking the mint authority, no new tokens can ever be minted. Revocation is permanent and cannot be undone.

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

---

### Prepare vs Execute
Source: https://platform.solana.com/docs/tokens/prepare-vs-execute

> Understand SDP's two signing modes for onchain transactions.

Every SDP endpoint that produces a Solana transaction supports two signing modes. The mode you choose determines who signs and submits the transaction.

## Execute mode (default)

Call a mutation endpoint without the `/prepare` suffix and SDP handles everything:

1. Builds the transaction
2. Signs it using your configured custody provider
3. Submits it to Solana
4. Returns the confirmed result with a transaction signature

```typescript
const res = await fetch(
  "https://api.solana.com/v1/issuance/tokens/tok_abc123/mint",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_test_...",
      "Content-Type": "application/json",
      "Idempotency-Key": "mint-001",
    },
    body: JSON.stringify({
      mint: { destination: "7xKXz...9fGh", amount: "1000" },
    }),
  }
);
const { data } = await res.json();
// data.transaction.signature — confirmed onchain
```

Issuance `amount` fields use **UI units** (decimal strings such as `"1000"` for one thousand tokens). SDP converts using the token's `decimals` before building the on-chain transaction.

**When to use execute mode:**

- You trust SDP's custody provider with signing
- You want the simplest integration — one API call, confirmed result
- Backend-to-backend workflows that don't require user signatures

## Prepare mode

Add `/prepare` to a mutation endpoint and SDP builds the transaction but returns it unsigned:

```typescript
const res = await fetch(
  "https://api.solana.com/v1/issuance/tokens/tok_abc123/mint/prepare",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      mint: { destination: "7xKXz...9fGh", amount: "1000" },
      options: { simulate: true },
    }),
  }
);
const { data } = await res.json();
// data.preparedTransaction.serialized — base64 transaction to sign
// data.simulation — { success, unitsConsumed, logs }
```

**When to use prepare mode:**

- You manage your own keys (hardware wallet, multisig, HSM)
- You need user approval before signing (wallet pop-up flow)
- You want to simulate the transaction before committing
- Regulatory requirements mandate specific signing infrastructure

## Signing a prepared transaction

After receiving the serialized transaction:

1. **Decode** the base64 string into a transaction object
2. **Sign** with the required private key(s)
3. **Submit** to Solana via `sendRawTransaction`
4. **Confirm** the transaction using `confirmTransaction`

Submit before `lastValidBlockHeight` expires. If the blockhash expires, call `/prepare` again for a fresh transaction.

## Simulation

Prepare endpoints accept `options.simulate: true` to dry-run the transaction before signing. The simulation returns:

- `success` — whether the transaction would succeed
- `unitsConsumed` — compute units used
- `logs` — program execution logs
- `error` — error details if simulation fails

## Endpoints that support both modes

| Resource | Execute | Prepare |
| --- | --- | --- |
| Deploy token | `POST .../deploy` | `POST .../deploy/prepare` |
| Mint | `POST .../mint` | `POST .../mint/prepare` |
| Burn | `POST .../burn` | `POST .../burn/prepare` |
| Force-burn | `POST .../force-burn` | `POST .../force-burn/prepare` |
| Seize | `POST .../seize` | `POST .../seize/prepare` |
| Update authority | `POST .../authority` | `POST .../authority/prepare` |

Issuance paths are prefixed with `https://api.solana.com/v1/issuance/tokens/{tokenId}`.

## Idempotency

Both modes support the `Idempotency-Key` header. For execute mode, the key prevents duplicate submissions. For prepare mode, the key ensures you get the same prepared transaction on retry.

---

## Wallet operations

### Wallet operations
Source: https://platform.solana.com/docs/wallet-operations

> Configure custody-wallet controls, operation permissions, and balances.

Wallet operations are the controls and observability attached to a custody wallet, independent of the product flow that uses it. A policy can govern payments, ramps, issuance, and signing operations when SDP evaluates them for the selected wallet.

## What's in this section

- [Wallet policies](/docs/wallet-operations/policies) — configure default decisions, destination controls, amount limits, operation permissions, approval requirements, and audit visibility.
- [Wallet balances](/docs/wallet-operations/balances) — retrieve token balances and optional USD valuations for a custody wallet.

## API surface

The current wallet-policy and balance endpoints remain under `/v1/payments/wallets` because they were introduced with the Payments API. Their behavior is wallet-wide: policy operation families include payment, ramp, issuance, raw signing, program interactions, and provider administration.

## Related

- [Set up wallets](/docs/guides/setup-wallets) — provision the wallet before configuring controls.
- [Payments](/docs/payments) — payment and ramp flows that can use a policy-controlled wallet.
- [Manage API keys](/docs/guides/manage-api-keys) — configure caller permissions separately from wallet operation permissions.

---

### Wallet policies
Source: https://platform.solana.com/docs/wallet-operations/policies

> Wallet controls, operation permissions, and policy audit visibility.

Wallet policies constrain how a custody wallet can move funds and execute supported operations. SDP starts from default allow, then applies the active wallet control profile and any policy fields you configure. Policies are managed via `GET` and `PUT` on `/v1/payments/wallets/{walletId}/policies` and enforced before wallet operations execute.

Use policies for treasury wallets, automated payout wallets, or any custody wallet where unbounded outbound flow would be a liability.

## Reading a wallet's policies

<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl https://api.solana.com/v1/payments/wallets/wal_abc123/policies \
  -H "Authorization: Bearer sk_test_..."
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/wallets/wal_abc123/policies",
  { headers: { Authorization: "Bearer sk_test_..." } }
);
const { data } = await response.json();
// data.policy.destinationAllowlist: string[]
// data.policy.maxTransferAmount?: string
// data.policy.maxDailyAmount?: string
// data.policy.controlProfile?.providerMappingStatus
// data.policy.audit?.recentEvaluations
```
</Tab>
</Tabs>

The payload is wrapped in the standard `data` / `meta` envelope, with the policy under a `policy` key:

```json
{
  "data": {
    "policy": {
      "walletId": "wal_abc123",
      "destinationAllowlist": ["7xKXz...9fGh", "5aBCd...2eFg"],
      "maxTransferAmount": "100.00",
      "maxDailyAmount": "1000.00",
      "defaultAction": "allow",
      "controlProfile": {
        "id": "wcp_abc123",
        "status": "active",
        "activeRevisionId": "wcpr_abc123",
        "revisionId": "wcpr_abc123",
        "revisionNumber": 3,
        "defaultAction": "allow",
        "rules": [
          {
            "id": "approval-required",
            "kind": "approval",
            "families": ["payment", "ramp"],
            "action": "approval_required",
            "name": "Approval checks"
          }
        ],
        "providerMappingStatus": "not_applicable",
        "createdAt": "2026-05-14T08:00:00Z",
        "updatedAt": "2026-05-14T10:15:00Z",
        "activatedAt": "2026-05-14T10:15:00Z"
      },
      "audit": {
        "recentEvaluations": [
          {
            "walletOperationId": "wop_abc123",
            "policyEvaluationId": "peval_abc123",
            "operationFamily": "payment",
            "operationType": "payment_transfer_execute",
            "asset": "USDC",
            "amount": "100.00",
            "destination": "7xKXz...9fGh",
            "status": "pending_approval",
            "decision": "approval_required",
            "reasonCode": "wallet_policy_match",
            "reason": "Payment matched approval policy.",
            "requiresApproval": true,
            "approvalRequestId": "appr_abc123",
            "operationCreatedAt": "2026-05-14T10:20:00Z",
            "operationUpdatedAt": "2026-05-14T10:20:00Z",
            "evaluatedAt": "2026-05-14T10:20:00Z"
          }
        ]
      },
      "createdAt": "2026-05-14T08:00:00Z",
      "updatedAt": "2026-05-14T10:15:00Z"
    }
  },
  "meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" }
}
```

Amount fields are **UI-unit decimal strings** (`"100.00"` means 100 tokens, regardless of the mint's decimals — matching the transfer-request convention). The configured value is a **single threshold**, but enforcement is **per token**: `maxDailyAmount` sums each token's outbound transfers separately within the day window, so a wallet with `maxDailyAmount: "1000"` can send up to 1000 USDC *and* 1000 SOL in the same UTC day — there is no aggregate cap across mints. If you need different per-token thresholds (e.g., 10,000 USDC daily but 50 SOL daily), partition into separate wallets.

## Default allow and active controls

Wallet policy evaluation starts from default allow. If no wallet control profile exists, supported wallet operations continue to run unless another product-specific check rejects them. When you activate a control profile, SDP evaluates its rules first and returns the resulting `decision` as one of `allow`, `deny`, `approval_required`, `provider_approval_required`, `review`, or `not_evaluated`.

The `controlProfile` object shows the active immutable revision. Treat `revisionId` and `revisionNumber` as audit references: a policy evaluation records which revision was active when the wallet operation was checked.

### Dashboard actions

The dashboard authoring flow exposes three actions: **Allow**, **Deny**, and **Require approval**. Choose **Require approval** when an operator must decide whether an operation can continue; it holds the operation and creates an approval request.

`review` remains a possible API evaluation result for legacy or safety paths, such as an unrecognized policy rule. It is visible in audit records, but it is not a dashboard authoring option or a separate human-review workflow. Do not use it to model operator approval; use `approval_required` instead.

## Operation permissions

Wallet policies can match operations at two levels:

- **Operation family** rules apply to every evaluated operation in a broad product area.
- **Operation type** rules match one exact operation identifier and take precedence when you need a narrower decision.

These are wallet-policy selectors, not API-key permissions. API-key permissions such as `payments:write` and `tokens:write` determine whether a caller may invoke an endpoint; operation permissions determine what the selected wallet may do after the request is authorized. See [Manage API keys](/docs/guides/manage-api-keys) for caller permissions.

### Operation families

| Family | Description | Current evaluated operations |
| --- | --- | --- |
| `payment` | Outbound single and batch payments. | Single and batch transfer execution |
| `ramp` | Fiat-to-crypto and crypto-to-fiat ramp activity. | On-ramp and off-ramp quote creation |
| `issuance` | Token lifecycle actions signed by the wallet. | Mint execution and authority updates |
| `raw_sign` | Low-level signing and signer verification. | Custody signer checks |
| `transfer` | Generic direct-transfer family reserved for operations outside the payment workflow. | No current public operation type |
| `program` | General Solana program interactions. | No current public operation type |
| `provider_admin` | Custody-provider administration. | No current public operation type |

Family values with no current public operation type are valid policy values, but they do not match a public SDP flow until an operation is emitted in that family.

### Exact operation types

| Operation type | Family | What it controls |
| --- | --- | --- |
| `payment_transfer_execute` | `payment` | Executes one outbound payment transfer from the wallet. |
| `payment_transfer_batch_execute` | `payment` | Executes an outbound batch and evaluates the aggregate amount before submission. |
| `ramp_onramp_quote` | `ramp` | Creates an on-ramp quote that delivers crypto to the destination wallet. |
| `ramp_offramp_quote` | `ramp` | Creates an off-ramp quote that draws crypto from the source wallet. |
| `issuance_mint_execute` | `issuance` | Executes a token mint using the configured signing wallet. |
| `issuance_update_authority_execute` | `issuance` | Changes a token authority using the current authority wallet. |
| `custody_signer_check` | `raw_sign` | Runs the custody signer-check transaction for the wallet. |

Operation-type matching is exact and case-sensitive. The API accepts custom strings up to 120 characters so future or privately integrated operations can be represented, but a rule only affects an operation when SDP emits the same identifier. For current public flows, use one of the identifiers above.

## Policy evaluation and audit records

Each supported wallet operation records a wallet operation row before policy evaluation and a policy evaluation row after SDP decides what should happen. `audit.recentEvaluations` exposes the most recent decisions for the wallet so support and customer operators can review:

- what operation was evaluated (`walletOperationId`, `operationFamily`, `operationType`)
- what decision SDP made (`decision`, `reasonCode`, `reason`, `requiresApproval`)
- whether an approval request was created (`approvalRequestId`)
- when the operation was created, updated, and evaluated

Use these records to explain allow, deny, approval-required, and review outcomes. They are an operational audit summary, not the full raw provider payload.

## Provider mapping status

`providerMappingStatus` describes whether an SDP policy revision has also been mapped into a custody or ramp provider's native policy system. Current payment wallet controls are SDP-enforced first. A value of `not_applicable` means there is no provider-native mapping for that revision. `pending`, `partial`, or `failed` mean SDP still evaluates the operation, but you should not assume the same rule has been fully synced to the provider.

## Destination allowlist

The allowlist is a list of on-chain destination addresses (32-44 character base-58 Solana pubkeys). When set, `POST /v1/payments/transfers` rejects any destination not in the list.

- Maximum entries: **500 addresses per wallet**.
- Each entry is a single on-chain address — no patterns, ranges, or address books.
- Empty allowlist (`[]`) means no destination restriction. To enable enforcement, populate at least one entry.

The allowlist exists to bound risk on automated wallets (a hot wallet that should only ever pay a known set of counterparties) and to enforce treasury controls (only the corporate cold-storage address can drain the operating wallet).

## Transfer limits

Two limits, both optional:

- **`maxTransferAmount`** — per-transfer cap. Any single `POST /v1/payments/transfers` exceeding this amount is rejected. Compared against the request's `amount` directly, so it is naturally per-token.
- **`maxDailyAmount`** — UTC-calendar-day cap, **enforced per token**. The day window resets at 00:00 UTC; the projected total is the sum of that wallet's outbound transfers for the **same token** in the current day (`pending`/`processing`/`confirmed`/`finalized`) plus the new request's `amount`. Different tokens are summed independently — see the per-token note under [Reading a wallet's policies](#reading-a-wallets-policies).

Set both for defense in depth: a per-transaction cap that catches obvious mistakes, plus a daily cap that bounds blast radius if many small transfers are submitted in a coordinated attack.

## Updating policies

`PUT /v1/payments/wallets/{walletId}/policies` has **full-replace** semantics — the request body becomes the new policy state in its entirety. To remove the allowlist, send `destinationAllowlist: []`. To drop a transfer limit, omit the field on the next PUT.

<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl -X PUT https://api.solana.com/v1/payments/wallets/wal_abc123/policies \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "destinationAllowlist": [
      "7xKXz...9fGh",
      "5aBCd...2eFg"
    ],
    "maxTransferAmount": "100.00",
    "maxDailyAmount": "1000.00"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
await fetch(
  "https://api.solana.com/v1/payments/wallets/wal_abc123/policies",
  {
    method: "PUT",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      destinationAllowlist: ["7xKXz...9fGh", "5aBCd...2eFg"],
      maxTransferAmount: "100.00",
      maxDailyAmount: "1000.00",
    }),
  }
);
```
</Tab>
</Tabs>

Because PUT is full-replace, treat policy edits like ordinary form submissions: read the current state, present it for editing, and PUT the complete updated object. Don't construct partial patches.

## Interaction with compliance

Wallet policies enforce **structural** constraints (where funds can go, how much, how fast). They do not replace compliance screening. If your organization has compliance screening enabled, screening happens on the destination address as a separate check, in addition to the allowlist match. A transfer can be rejected by either layer; both must pass to reach the network.

See the [Compliance API reference](/docs/reference/api/compliance) for address-screening flows that complement policies.

## Related

- [Payouts and disbursements](/docs/payments/send-payouts) — running an outbound batch from a policy-constrained wallet.
- [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint subject to policy enforcement.
- [Wallet balances](/docs/wallet-operations/balances) — current balance, useful to size transfer limits.

---

### Wallet balances
Source: https://platform.solana.com/docs/wallet-operations/balances

> Per-wallet token balances with optional USD valuation via SDP.

`GET /v1/payments/wallets/{walletId}/balances` returns the wallet's native **SOL** balance plus every **SPL token** balance held by a custody wallet — raw amount, human-readable amount, decimals, and optional USD valuation when a price source is configured. SOL is always prepended as the first entry and is represented with `token: "SOL"` and `mint: "So11111111111111111111111111111111111111112"`.

## Request

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl https://api.solana.com/v1/payments/wallets/wal_abc123/balances \
  -H "Authorization: Bearer sk_test_..."
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/wallets/wal_abc123/balances",
  { headers: { Authorization: "Bearer sk_test_..." } }
);
const { data } = await response.json();
// data.walletBalances.balances: TokenBalance[]
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/wallets/wal_abc123/balances"))
    .header("Authorization", "Bearer sk_test_...")
    .GET()
    .build();
```
</Tab>
</Tabs>

## Response shape

The payload is wrapped in the standard `data` / `meta` envelope, with the balances under a `walletBalances` key:

```json
{
  "data": {
    "walletBalances": {
      "walletId": "wal_abc123",
      "address": "3xYZa...2aBc",
      "balances": [
        {
          "token": "SOL",
          "mint": "So11111111111111111111111111111111111111112",
          "amount": "2500000000",
          "uiAmount": "2.5",
          "decimals": 9
        },
        {
          "token": "USDC",
          "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
          "amount": "1500000000",
          "uiAmount": "1500.00",
          "decimals": 6,
          "usdPrice": 1.0,
          "usdValue": 1500.0
        }
      ]
    }
  },
  "meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" }
}
```

Per-balance fields:

| Field | Notes |
| --- | --- |
| `token` | Token symbol if known, otherwise the on-chain mint. |
| `mint` | On-chain mint address. |
| `amount` | Raw amount in the smallest unit (string to preserve precision). |
| `uiAmount` | Human-readable decimal string — `amount / 10^decimals`. |
| `decimals` | Token's decimal count. |
| `usdPrice` | Per-token USD price; omitted when no pricing data is available. |
| `usdValue` | Total USD value (= `usdPrice * uiAmount`); omitted when pricing is unavailable. |
| `confidential` | Reserved for confidential transfer balances; absent for standard balances. |

## Use cases

- **Account dashboards** — display the contents of a custody wallet to its operator.
- **Pre-transfer balance checks** — validate that a wallet has enough of a specific token before kicking off a payout batch.
- **Treasury accounting** — periodic snapshots into an internal ledger.

## Pricing freshness

When USD valuation is populated, the price comes from SDP's configured price source and reflects the most recent quote available. The cadence and source depend on deployment configuration — do not rely on `usdValue` for downstream balance-sheet decisions without confirming the price source's freshness with your operations team. When in doubt, treat `amount` / `uiAmount` as authoritative and convert with your own price feed.

If no price source is configured, the `usdPrice` and `usdValue` fields are simply absent rather than zero — falling back to "I don't know" rather than "$0.00" so downstream consumers can decide how to handle the gap.

## Comparison to on-chain RPC

You can also enumerate SPL token balances by calling `getTokenAccountsByOwner` against a Solana RPC and decoding the returned token-account data. Reasons to use SDP's `balances` endpoint instead:

- **Token resolution** — SDP resolves the mint to a symbol when it knows one; raw RPC returns mints only.
- **USD pricing** — RPC has no concept of price.
- **No RPC dependency in your stack** — one fewer external integration when you are already calling SDP.

Reasons to skip SDP and go to RPC directly:

- **Cluster-of-truth** — you want the wallet's state at a specific slot for an audit.
- **Confidential transfer balances** — full extension support is RPC-side.
- **Latency-sensitive paths** — RPC is one hop closer to the cluster.

## Related

- [Wallet policies](/docs/wallet-operations/policies) — set transfer caps based on balance bounds.
- [Payouts and disbursements](/docs/payments/send-payouts) — balance-check before running a batch.
- [Set Up Wallets](/docs/guides/setup-wallets) — provision the wallets whose balances this endpoint reports.

---

## Payments

### Payments
Source: https://platform.solana.com/docs/payments

> Build payments on Solana with SDP — concepts, send, accept, and ramps.

SDP provides a Solana-native payments stack: stablecoin transfers, fiat on-ramps and off-ramps, and reconciliation primitives. This section mirrors the structure of the [public Solana payments documentation](https://solana.com/docs/payments) and maps each workflow to the SDP APIs you call to ship it.

If you have not yet provisioned a custody wallet or token, start with [Set Up Wallets](/docs/guides/setup-wallets) and [Tokenize an Asset](/docs/guides/tokenize-an-asset).

## What's in this section

- **[How payments work on SDP](/docs/payments/concepts)** — wallets, tokens, signing modes, fees, and the transfer data model.
- **Send payments** — outbound flows.
  - [Basic payment](/docs/payments/send-basic-payment) — a single transfer in either signing mode.
  - [Payment with memo](/docs/payments/send-payment-with-memo) — attach order or customer references to a transfer.
  - [Payouts and disbursements](/docs/payments/send-payouts) — batched outbound transfers and reconciliation.
- **Accept payments** — inbound flows.
  - [Accept overview](/docs/payments/accept-overview) — receiving-address convention and integration options.
  - [Verifying a payment](/docs/payments/accept-verification) — transfer-status semantics and finality.
  - [Indexing and reconciliation](/docs/payments/accept-indexing) — listing inbound transfers for your ledger.
- **Ramps** — fiat on-ramps and off-ramps.
  - [Ramps overview](/docs/payments/ramps) — onramp and offramp request shape, KYC handoff, status states.
  - [Ramp providers](/docs/payments/ramps-providers) — MoonPay, Lightspark, and BVNK configuration.

Wallet-wide controls and balances are documented in [Wallet operations](/docs/wallet-operations).

## Underlying API

Every page in this section links to specific endpoints in the auto-generated [Payments API reference](/docs/reference/api/payments). The Payments API surface is namespaced under `/v1/payments` and groups into four areas: `transfers`, `ramps`, `wallets/.../balances`, and `wallets/.../policies`.

## Related

- [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow) — high-level tutorial covering quote through settlement.
- [Prepare vs Execute](/docs/guides/prepare-vs-execute) — SDP's two signing modes (used throughout payments).

---

### How payments work on SDP
Source: https://platform.solana.com/docs/payments/concepts

> Wallets, tokens, server-side signing, fees, and the data model that backs SDP payments.

SDP payments move value between Solana accounts: a custody wallet sends tokens to a destination address, SDP records the transfer, and the rest of the section describes how to send those, accept incoming ones, ramp into and out of fiat, and reconcile activity against your product database.

This page introduces the building blocks the rest of the section assumes.

## Custody wallets

Every SDP payment is anchored to a **custody wallet** — a Solana keypair whose private key SDP holds, scoped to an organization and (optionally) a project. Wallets sign outbound transfers on your behalf and contribute to balances and policies for the addresses you control.

You provision custody wallets up front (see [Set Up Wallets](/docs/guides/setup-wallets)) and reference them by their SDP `wallet_id` (`wal_…`). Transfer endpoints expect the **source** as a custody wallet ID — the wallet is resolved server-side from the caller's custody-wallet list — and the **destination** as the on-chain Solana address of the recipient. Ramp endpoints vary by provider: MoonPay and BVNK accept either a wallet ID or a Solana address for `sourceWallet` / `destinationWallet`; Lightspark uses its own `ExternalAccount:…` identifiers (onramp `destinationWallet` also accepts a Solana address, offramp `sourceWallet` does not).

## Tokens and token accounts

Payments transfer **SPL tokens** — stablecoins, tokenized assets, or any mint your project supports. The `token` field on `POST /v1/payments/transfers` requires the on-chain **mint address** for SPL tokens; the literal string `SOL` is accepted as the native-SOL shorthand. Symbols like `USDC` are not resolved at request time. The `token` filter on `GET /v1/payments/transfers` does an **exact match** against the `Transfer.token` value as stored — API-created transfers store the mint you supplied (or `SOL`), while indexed/observed transfers run mint-to-symbol resolution and may store a resolved symbol (e.g. `USDC`). Filter using the same label you see on the response, or query without `token` and filter client-side. **Balance responses always attempt symbol resolution** and return the symbol whenever the mint is known — so a wallet holding USDC reports `USDC` rather than the mint string.

**Amounts on the wire are UI-unit decimal strings** — `"100.00"` means 100 tokens, regardless of the mint's decimal count. This applies to issuance operations (mint, burn, seize, force-burn) as well as payments transfers. SDP handles the smallest-unit conversion when it builds the on-chain transaction. The one exception is wallet-balance responses, which return both a raw `amount` (smallest unit) **and** a `uiAmount` (decimal string) so balance consumers can pick whichever fits.

## Transfer execution

Outbound wallet transfers are server-executed through `POST /v1/payments/transfers`. SDP builds the transaction, signs with the source wallet's custody key, submits it on the configured Solana cluster, and records the resulting transfer lifecycle.

The rest of this section assumes transfers are submitted by SDP from a custody wallet you control.

## Fees and sponsorship

Solana fees are paid in SOL by the transaction's fee payer. On wallet transfers, the source custody wallet is the fee payer by default; SDP can be configured to sponsor fees so end-user wallets do not need a SOL balance.

## The transfer data model

Every successful or attempted transfer creates a `Transfer` record. The fields that matter for most flows:

| Field | Meaning |
| --- | --- |
| `id` | SDP-internal transfer identifier (use this in API URLs). |
| `status` | One of `pending`, `processing`, `confirmed`, `finalized`, `failed`. See [Verifying a payment](/docs/payments/accept-verification). |
| `direction` | `inbound` (someone paid you) or `outbound` (you paid someone). |
| `source`, `destination` | On-chain addresses. |
| `token`, `amount` | What moved and how much. `amount` is a UI-unit decimal string (e.g. `"100.00"`). |
| `memo` | Optional UTF-8 string, up to 256 chars. See [Payment with memo](/docs/payments/send-payment-with-memo). |
| `signature` | Solana transaction signature once confirmed. **Unique across SDP** — the natural dedup key. |
| `slot`, `blockTime`, `fee` | On-chain settlement details, populated as the transaction lands. |
| `error` | Set if the transfer failed; the rest of the record explains what reached the network. |
| `risk` | Optional risk-score metadata, when a risk provider is configured. |

Outbound wallet transfers start at `processing` and move to `confirmed` → `finalized`. Inbound transfers discovered on-chain surface directly at `confirmed`. Ramp transfers can start at `pending` while provider-side steps are still underway. See [Verifying a payment](/docs/payments/accept-verification#status-lifecycle) for the full state table.

## What SDP does and does not give you

SDP exposes everything you need to **send**, **track**, and **reconcile** payments at the transfer level: status polling, filtered lists, wallet-level balances, and a UNIQUE `signature` on the transfer record for client-side dedup (see [Basic payment → Deduplication](/docs/payments/send-basic-payment#deduplication)). It does **not** ship higher-level commerce primitives — there is no checkout session, payment intent, or invoice object in the API today, no webhooks for settlement events, and payment endpoints do not honor an `Idempotency-Key` header (unlike issuance endpoints). The [Accept payments](/docs/payments/accept-overview) section shows how to build those abstractions on top of the transfer model.

## Related

- [Payments API reference](/docs/reference/api/payments) — auto-generated endpoint reference.
- [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow) — a step-by-step tutorial through the same lifecycle from a different angle.

---

## Payments — Send Payments

### Basic payment
Source: https://platform.solana.com/docs/payments/send-basic-payment

> Send a single token transfer with SDP.

SDP handles token transfers through its payments API. A basic payment moves tokens **from a custody wallet you control to any Solana wallet address**. The `source` field on the request is an SDP custody wallet ID (`wal_…`), resolved server-side from the caller's wallet list; `destination` is the recipient's on-chain **wallet (owner) address** — for SPL transfers, SDP derives the associated token account (ATA) from this address and creates it if needed, so pass the wallet pubkey, **not** a token-account address.

Transfers are available through the Payments dashboard and the API.

## Prerequisites

- A deployed token with minted supply
- A custody wallet to use as the `source`, with a custody signer configured so SDP can sign and submit.
- An API key with both `payments:write` and `wallets:read` permissions, where the key's `payments:write` scope includes the source wallet — `POST /v1/payments/transfers` is gated by that pair at the route layer, and the source wallet is then re-checked against the key's `payments:write` wallet scope.

## How it works

<HowItWorks>

<Step number={1} title="Set up the transfer">

Specify the source, destination, token, and amount.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Go to **Payments** in the sidebar. The overview shows your total SDP balance, recent transactions, and **Send** / **Receive** actions.

![Payments overview showing balance, Send and Receive buttons, and recent transactions](/images/tokens/transfer-confirmed.png)

Click **Send** and select **Wallet transfer** to send from an SDP wallet to a Solana address.

![Send page showing Wallet transfer and Off-ramp options](/images/tokens/transfer-send-options.png)

On the **Enter transfer details** form, select a source wallet from the dropdown.

![Transfer details form with source wallet dropdown open](/images/tokens/transfer-form-source.png)

Fill in:

- **Source** — a wallet you control in SDP
- **Amount** — human-readable units (e.g., `0.5`)
- **Asset** — select from your deployed tokens
- **Destination address** — any valid Solana wallet address
- **Memo** — optional note recorded onchain

![Transfer details form with all fields filled in](/images/tokens/transfer-form-filled.png)

</Tab>
<Tab value="API">

Build the request body:

```typescript
const body = {
  source: "privy_wallet_123",              // `walletId` from GET /v1/wallets
  destination: "7xKXz...9fGh",         // recipient wallet (owner) address
  token: "9aBCd...4eEf",               // onchain mint address
  amount: "100.00",                    // human-readable units
  memo: "Payment for invoice #1234",
};
```

`token` is the mint address returned by the deploy step, not the SDP token ID. `destination` is the wallet pubkey — SDP derives the associated token account from it.

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={2} title="Execute the transfer">

Submit the transfer. SDP signs with the source custody wallet and submits the transaction.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

Click **Review** to see the transfer summary, then **Confirm** to submit.

The transfer status updates to `Confirmed` once the transaction is finalized.

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "privy_wallet_123",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "100.00",
    "memo": "Payment for invoice #1234"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/transfers",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "privy_wallet_123",
      destination: "7xKXz...9fGh",
      token: "9aBCd...4eEf",
      amount: "100.00",
      memo: "Payment for invoice #1234",
    }),
  }
);
const { data } = await response.json();
// data.transfer.id, data.transfer.status — transfer record under the `transfer` envelope key
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "source": "privy_wallet_123",
          "destination": "7xKXz...9fGh",
          "token": "9aBCd...4eEf",
          "amount": "100.00",
          "memo": "Payment for invoice #1234"
        }"""))
    .build();
```
</Tab>
</Tabs>

</Tab>
</Tabs>
</StepPanel>

</Step>

<Step number={3} title="Verify the transfer">

Confirm the transfer completed and balances updated.

<StepPanel>
<Tabs items={["UI", "API"]} groupId="how-it-works">
<Tab value="UI">

The transfer appears in **Payments** history with a `confirmed` status and an onchain transaction signature.

![Payments overview showing confirmed inbound transaction](/images/tokens/transfer-confirmed.png)

Click the record to see full details including the Solana Explorer link.

</Tab>
<Tab value="API">

```typescript
// data.transfer.status    — "confirmed"
// data.transfer.signature — onchain tx signature
```

Poll status for async confirmations:

```bash
curl https://api.solana.com/v1/payments/transfers/txn_abc123 \
  -H "Authorization: Bearer sk_test_..."
```

</Tab>
</Tabs>
</StepPanel>

</Step>

</HowItWorks>

## Deduplication

Unlike the issuance endpoints, payments transfer endpoints do not currently honor an `Idempotency-Key` header — retrying a failed transfer request will produce a new `Transfer` record. To guarantee at-most-once execution from the client side:

1. Persist the SDP `Transfer.id` returned by the first successful response **before** retrying. On retry, skip submission if you already have a transfer ID for that logical payment.
2. Rely on the on-chain `signature` (UNIQUE across SDP's transfers table) as the cross-source dedup key **once it is populated** on the record. `signature` is filled in when the transaction is submitted to the network, which can lag behind the initial `processing` status — treat its presence (not the `status` value) as the trigger for signature-based dedup.

See [Payouts and disbursements](/docs/payments/send-payouts#dedup-and-retries) for the same pattern applied to batched outbound flows.

## Related

- [Payment with memo](/docs/payments/send-payment-with-memo) — using the `memo` field to carry order references
- [Payouts and disbursements](/docs/payments/send-payouts) — batched outbound flows
- [Mint and Burn](/docs/guides/mint-and-burn) — create or destroy token supply
- [Manage Allowlists](/docs/guides/manage-allowlists) — define approved destinations for allowlist-enabled issuance flows
- [Freeze and Compliance](/docs/guides/freeze-and-compliance) — halt transfers for compliance

---

### Payment with memo
Source: https://platform.solana.com/docs/payments/send-payment-with-memo

> Attach an order ID, invoice number, or customer reference to an SDP transfer.

The `memo` field on `POST /v1/payments/transfers` carries an arbitrary UTF-8 string (up to 256 characters) alongside the on-chain transfer. Memos are persisted on the SDP `Transfer` record and surface in list and read responses, so your indexer can map each transfer back to a row in your product database.

## When to use a memo

Use a memo when you need a free-form correlation token that your backend already knows at request time:

- **Order or invoice IDs** — `order_2026-05-14_4837` so the inbound transfer ties to a checkout row.
- **Customer references** — internal customer or account identifiers for support and reconciliation.
- **Run identifiers** — a payout-batch ID shared across many outbound transfers in the same disbursement.

Memos are stored by SDP. They are **not** automatically attached as a Solana memo-program instruction; if you need an on-chain memo that block explorers display, that has to be handled at the transaction level (see the trade-offs section below).

## API usage

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "privy_wallet_123",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "100.00",
    "memo": "order_2026-05-14_4837"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/transfers",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "privy_wallet_123",
      destination: "7xKXz...9fGh",
      token: "9aBCd...4eEf",
      amount: "100.00",
      memo: "order_2026-05-14_4837",
    }),
  }
);
const { data } = await response.json();
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "source": "privy_wallet_123",
          "destination": "7xKXz...9fGh",
          "token": "9aBCd...4eEf",
          "amount": "100.00",
          "memo": "order_2026-05-14_4837"
        }"""))
    .build();
```
</Tab>
</Tabs>

The memo is preserved on the transfer record:

```json
{
  "id": "xfr_...",
  "memo": "order_2026-05-14_4837",
  "status": "finalized",
  "signature": "5xR1...",
  ...
}
```

Memo limits: `z.string().max(256)`. UTF-8 is accepted; trim or sanitize on the way in if your downstream systems are stricter.

## Trade-offs vs. Solana Pay reference accounts

For accept flows, **Solana Pay reference accounts** are an alternative correlation mechanism: you generate a unique pubkey per order and include it in the on-chain transfer as a reference. The advantage is on-chain provenance — anyone can verify the link without trusting SDP's database — and the ability for the payer to construct the transfer themselves from a Solana Pay URL.

SDP's `Prepare` endpoint accepts a `referenceAddress` field as a roadmap hook for Solana Pay-style correlation, but **does not yet attach it on-chain or expose it on inbound transfer records** — for now use `memo` for SDP-side correlation, or read the reference directly from the transaction via Solana RPC. Use [Accept overview](/docs/payments/accept-overview) and [Indexing and reconciliation](/docs/payments/accept-indexing) for the inbound side.

Rule of thumb: memos are simpler when **you** initiate the transfer (server-side payouts, internal moves), references are stronger when **a third party** initiates the transfer (customer checkout, donations).

## Indexing implications

Memos are returned by `GET /v1/payments/transfers` and `GET /v1/payments/transfers/{id}`, so your reconciliation worker can filter by date / direction / token and then match each row to your order table by memo. See [Indexing and reconciliation](/docs/payments/accept-indexing) for query patterns.

## Related

- [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint.
- [Payouts and disbursements](/docs/payments/send-payouts) — using memos to tag a batch.
- [Accept overview](/docs/payments/accept-overview) — when to reach for reference accounts instead.

---

### Payouts and disbursements
Source: https://platform.solana.com/docs/payments/send-payouts

> Run batched outbound transfers, deduplicate retries, and reconcile a payout batch with SDP.

A payout run is a batch of outbound transfers — marketplace seller payouts, payroll, affiliate disbursements, royalty distributions. SDP does not expose a "batch" object; you call `POST /v1/payments/transfers` once per recipient, tag each transfer with a shared batch identifier, and use `GET /v1/payments/transfers?direction=outbound` to reconcile the run.

This page covers the recommended pattern: how to structure the loop, how to deduplicate on retries, and how to confirm every recipient was paid.

## Designing a payout run

A typical pattern:

1. **Resolve recipients** in your application — list (or stream) the rows to be paid, with destination address and amount per row.
2. **Pick a batch identifier** you control (`payout_2026-05-14_marketplace_sellers`).
3. **Iterate** — for each recipient, call `POST /v1/payments/transfers` with the destination, amount, token, and a memo that encodes the batch ID and a per-row identifier (`payout_2026-05-14_marketplace_sellers/row_4837`).
4. **Capture** the returned `Transfer.id` and `Transfer.status`. Persist the SDP transfer ID alongside the recipient row so you can re-poll status later.
5. **Reconcile** — once the batch is fully submitted, run `GET /v1/payments/transfers?direction=outbound&from=…&to=…` and confirm every row in your application has a matching `finalized` transfer.

Send transfers sequentially or in modest concurrency (4–8 in flight); high concurrency increases the chance of priority-fee contention and blockhash expiry without much throughput gain.

## Dedup and retries

Two layers protect you against double-spends if a worker dies mid-run:

1. **Application-level keys** — persist the SDP transfer ID for each recipient row *before* moving to the next row. On retry, skip rows that already have an associated `Transfer.id`. This is the dedup mechanism you should rely on most.
2. **Transfer-record uniqueness** — the on-chain `signature` column is `UNIQUE` across the SDP `payment_transfers` table. Each transfer request inserts its own row first and the signature is recorded once the transaction is built/submitted, so a duplicate signature is blocked at the DB layer (the second write errors out) rather than silently merged. Use this as a backstop integrity check, not as your primary dedup — the application-level keys above are what you actually rely on.

If your retry path replays a transfer with identical inputs but a fresh request, SDP builds a new transaction with a new recent blockhash, which means a new signature. The application-level key — persisted recipient → transfer ID mapping — is what prevents the recipient from being paid twice in that case.

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "privy_wallet_123",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "150.00",
    "memo": "payout_2026-05-14_marketplace_sellers/row_4837"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
async function payoutRecipient(row: PayoutRow): Promise<string> {
  if (row.transferId) return row.transferId; // already submitted

  const response = await fetch(
    "https://api.solana.com/v1/payments/transfers",
    {
      method: "POST",
      headers: {
        "Authorization": "Bearer sk_test_...",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        source: row.sourceWallet,
        destination: row.destination,
        token: row.token,
        amount: row.amount,
        memo: `${row.batchId}/row_${row.id}`,
      }),
    }
  );

  const { data } = await response.json();
  // The transfer record is under data.transfer (standard SDP success envelope).
  await row.persistTransferId(data.transfer.id); // persist BEFORE returning
  return data.transfer.id;
}
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "source": "%s",
          "destination": "%s",
          "token": "%s",
          "amount": "%s",
          "memo": "%s/row_%s"
        }""".formatted(sourceWallet, destination, token, amount, batchId, rowId)))
    .build();
```
</Tab>
</Tabs>

## Reconciling a run

After the loop finishes, list all outbound transfers in the run's time window and tally against your recipient table:

<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl "https://api.solana.com/v1/payments/transfers?direction=outbound&from=2026-05-14T00:00:00Z&to=2026-05-14T23:59:59Z&pageSize=100" \
  -H "Authorization: Bearer sk_test_..."
```
</Tab>
<Tab value="TypeScript">
```typescript
async function* listOutboundTransfers(from: string, to: string) {
  let page = 1;
  while (true) {
    const url = new URL("https://api.solana.com/v1/payments/transfers");
    url.searchParams.set("direction", "outbound");
    url.searchParams.set("from", from);
    url.searchParams.set("to", to);
    url.searchParams.set("page", String(page));
    url.searchParams.set("pageSize", "100");

    const res = await fetch(url, {
      headers: { Authorization: "Bearer sk_test_..." },
    });
    const { data, meta } = await res.json();
    yield* data;
    if (!meta.hasMore) return;
    page += 1;
  }
}
```
</Tab>
</Tabs>

Filter the returned transfers on `memo.startsWith(batchId)` (or whatever convention you chose) to scope to the batch. Cross-reference each recipient row by SDP `Transfer.id`.

## Failure modes

Per-transfer outcomes you should expect to handle:

- **`failed`** — the transaction was built and submitted but the network rejected it (insufficient funds, frozen account, expired blockhash). The `error` field on the transfer record explains what reached the chain. The recipient was not paid; you can retry by submitting a new transfer.
- **`pending` past the expected confirmation window** — usually a backend or RPC hiccup. Re-poll with `GET /v1/payments/transfers/{id}` rather than re-submitting; you only want a new transfer if `status` settles to `failed`.
- **Mid-run worker crash** — application-level keys (persisted recipient → transfer ID) are how you resume safely. Skip rows that already have a `Transfer.id` and resume the loop.
- **Source wallet runs out of SOL for fees** — submissions start failing with fee-related errors. If SDP is configured to sponsor fees this won't happen; otherwise top up the source wallet's SOL balance before continuing.

## Related

- [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint.
- [Payment with memo](/docs/payments/send-payment-with-memo) — the memo field used above to tag a batch.
- [Indexing and reconciliation](/docs/payments/accept-indexing) — the same list endpoint, used inbound.
- [Wallet policies](/docs/wallet-operations/policies) — destination allowlist and daily-limit guardrails for payout wallets.

---

## Payments — Accept Payments

### Accept payments
Source: https://platform.solana.com/docs/payments/accept-overview

> Receiving-address convention, integration options, and how to track inbound payments with SDP.

SDP does not ship a hosted checkout primitive. To accept payments today, you publish a Solana address you control and detect inbound transfers via the SDP transfer list. This page explains the pattern; the two pages that follow cover [verification](/docs/payments/accept-verification) and [indexing](/docs/payments/accept-indexing) in detail.

## Receiving-address convention

The receiving address is a custody wallet you control. Two scopings to choose between:

- **One wallet per order** *(recommended for accept flows)* — provision a fresh custody wallet (or sub-account) per order or per customer and use the destination address itself as the correlation key. The inbound transfer's `destination` field maps 1:1 to your order row, so you do not depend on memo or reference fields that SDP does not surface for inbound payments.
- **One wallet per merchant scope** — for marketplaces, one custody wallet per seller; for treasury operations, one per ledger account. Reasonable when correlation isn't required (treasury, settlement) or when you accept a single fixed-amount payment per merchant.

The address you publish is the on-chain `address` of the custody wallet. Fetch it with [`GET /v1/wallets/{walletId}`](/docs/reference/api/wallets); when you only need wallet metadata, add `?includeBalance=false` to avoid a balance RPC and pricing lookup. You publish that address; senders use it as the destination of a normal SPL token transfer. SDP indexes the inbound transfer and exposes it in [`GET /v1/payments/transfers`](/docs/payments/accept-indexing) with `direction=inbound`.

**Correlation caveats.** For *customer-initiated* inbound transfers, SDP today does **not** populate `Transfer.memo` (Solana memo-program instructions aren't extracted into the record) and does **not** expose a reference field on the inbound record. The reliable on-record correlation key is the inbound transfer's `destination` — hence the per-order receiving-address recommendation. If you need memo- or reference-based correlation (e.g., for a Solana Pay URL that carries an order pubkey), read the on-chain transaction directly via Solana RPC using the transfer's `signature`.

## Integration options

There are two ways customers can land a payment on the address you control:

1. **Direct address share** — you display the address (and the expected token + amount) in your UI. The customer's wallet sends a transfer. This works with any Solana wallet and any client; SDP picks the transfer up purely from the on-chain side. Best for B2B settlement, treasury operations, and crypto-native flows.
2. **Solana Pay request** — for consumer checkout, you give the customer a [Solana Pay](https://solana.com/docs/payments/accept-payments/solana-pay) URL or QR code with the destination, amount, and a per-order `reference` pubkey. The customer's wallet builds the transfer and attaches the `reference` account on-chain, so you can match the transfer back to the order by reading the transaction directly via Solana RPC (the `reference` won't be on the SDP `Transfer` record — neither will any memo the wallet sets, since SDP doesn't extract memo-program instructions for inbound transfers). SDP does not ship a hosted Solana Pay endpoint today. The [`Prepare`](/docs/guides/prepare-vs-execute) transfer endpoint accepts a `referenceAddress` field as a roadmap hook, but **does not yet attach it on-chain or echo it on the inbound transfer record** (see [Indexing and reconciliation → Solana Pay reference accounts](/docs/payments/accept-indexing#solana-pay-reference-accounts-roadmap)). For SDP-side correlation today, use **per-order destination addresses** (covered above); use the on-chain transaction (via RPC) when you need to read memo or `reference`.

## What SDP gives you today

- **Indexed inbound transfers** — `GET /v1/payments/transfers?direction=inbound` returns all received transfers across your custody wallets, with filters for token, status, wallet, and date range. See [Indexing and reconciliation](/docs/payments/accept-indexing).
- **Per-transfer status reads** — `GET /v1/payments/transfers/{id}` for a single payment's lifecycle. See [Verifying a payment](/docs/payments/accept-verification).
- **Wallet balances** — `GET /v1/payments/wallets/{walletId}/balances` for the current per-token balance on a receiving wallet. See [Wallet balances](/docs/wallet-operations/balances).
- **Address-based correlation** — the inbound transfer's `destination` is the on-record correlation key for accept flows. Pair with per-order receiving addresses (see above) to map inbound transfers to orders without depending on memo. `Transfer.memo` is populated only for SDP-initiated transfers (outbound or internal); for customer-initiated inbound transfers the field is **omitted** from the response (it's optional in the schema, not nullable). Read the on-chain transaction via Solana RPC if you need memo or Solana Pay reference correlation.

## What SDP does not give you yet

- **No checkout session / payment intent object** — you do not pre-register an expected payment with the API. Inbound transfers are recognized after the fact by matching the on-record `destination` (token and amount are available for additional sanity checks). Memo and Solana Pay reference aren't on the transfer record for customer-initiated inbound transfers — see the [correlation caveats](#receiving-address-convention) above; read those from the on-chain transaction via Solana RPC if you need them.
- **No webhooks on settlement** — status changes are observed by polling `GET /v1/payments/transfers/{id}` or by re-listing with date filters. See the [polling cadence guidance](/docs/payments/accept-verification#polling-cadence).
- **No hosted Solana Pay URL endpoint** — if you want to ship a Solana Pay URL or QR, build it client-side from the destination address, token mint, amount, and reference pubkey.

## Worked example: a marketplace checkout

A minimum-viable accept flow built on the per-order-address pattern:

1. At checkout, provision (or claim from a warm pool) a **fresh custody wallet** for the order and stage an `orders` row with that wallet's on-chain address, the expected amount, and the token.
2. Display the per-order custody-wallet address, the token, and the amount to the customer. Build a Solana Pay URL from those fields if you want a QR code; no SDP-specific endpoint required.
3. The customer's wallet submits the transfer.
4. Your reconciliation worker (see [Indexing and reconciliation](/docs/payments/accept-indexing)) periodically lists recent inbound transfers — **without** a `wallet`/`walletAddress` filter (the wallet-scoped path's signature-history branch ignores the `from`/`to` window; see [Indexing → Query parameters](/docs/payments/accept-indexing#query-parameters)) — and matches each transfer's `destination` to an open order in your `orders` table.
5. When a match lands, mark the order paid and ship the goods. Use the transfer's `signature` (UNIQUE on the transfer record once present) to dedup if the worker is re-run.

This is intentionally low-level — there is no SDP-provided session object. Treat the `orders` table as your checkout state machine and SDP as the settlement and status backend.

## Next steps

- [Verifying a payment](/docs/payments/accept-verification) — status semantics and how to know when a payment is safe to act on.
- [Indexing and reconciliation](/docs/payments/accept-indexing) — listing inbound transfers, deduping, and building a delta-poll worker.

---

### Verifying a payment
Source: https://platform.solana.com/docs/payments/accept-verification

> Transfer-status semantics, finality on Solana, and recommended polling cadence for SDP transfers.

When a payment lands on a wallet you control, the inbound transfer moves through a handful of status states. This page describes those states, what each one means about on-chain finality, and how to decide when it is safe to ship goods or release funds.

## Status lifecycle

The `status` field on the `Transfer` record takes one of five values:

| Status | Meaning |
| --- | --- |
| `pending` | Provider-backed ramp transfer setup is still underway. Wallet transfers created with `POST /v1/payments/transfers` do not start here; they start at `processing`. No wallet-transfer `signature` yet. |
| `processing` | The transfer is in flight. For outbound wallet transfers this is the **initial state** at record creation: the request has been accepted and the transaction is being built and submitted; the `signature` field is not yet populated when the record is first written. |
| `confirmed` | The cluster has confirmed the transaction. `signature` and `slot` are populated. `blockTime` may also be set, but is not guaranteed at this stage — treat it as optional until `finalized`. |
| `finalized` | The slot containing the transaction is locked in by the network — finality at the Solana cluster's strongest guarantee. `signature` and `slot` are populated; `blockTime` is best-effort and may remain `null` for wallet transfers whose finalization is observed by the `track-pending-transfers` job (which updates status and slot but does not backfill block time). When you need a definitive timestamp, resolve it from the on-chain `signature` via Solana RPC. Safe to treat as irreversible. |
| `failed` | The transaction was built and submitted but rejected by the network (insufficient funds, frozen account, expired blockhash, etc.). The `error` field describes what reached the chain. |

A wallet transfer created with `POST /v1/payments/transfers` moves forward through `processing → confirmed → finalized`. Ramp transfers may begin at `pending` before moving into provider-specific processing. **Inbound transfers** discovered via on-chain signature history surface directly at `confirmed` (or `failed` if the on-chain transaction errored) — they never appear as `pending`. Any state can transition to `failed`; `failed` is terminal.

## Finality on Solana

For real-money flows, treat `finalized` as the safe-to-act-on signal. `confirmed` is durable enough for most non-adversarial workflows (think: showing a "paid" badge in your dashboard) but a sufficiently motivated cluster reorganization can theoretically roll back a `confirmed` transaction; this does not happen for `finalized` transactions.

Use `confirmed` to release low-stakes side effects (status emails, UI updates) and `finalized` for irreversible side effects (releasing custody, shipping high-value goods, posting to your general ledger).

## Polling cadence

SDP does not currently emit settlement webhooks. Status is observed by polling:

- `GET /v1/payments/transfers/{id}` for a single transfer's current state.
- `GET /v1/payments/transfers?direction=inbound&from=…` for a windowed list — see [Indexing and reconciliation](/docs/payments/accept-indexing).

Cadence depends on how quickly you need to see state transitions and how aggressive your retry budget is. A reasonable starting range:

- **Active checkout** (customer is staring at a payment screen): every 2–5 seconds for up to a minute, then back off.
- **Background reconciliation** (no one is waiting): every 15–30 seconds, or whenever a worker tick fires.
- **Long-tail catch-up** (status reads after the fact): single one-shot reads when your application needs to act, no polling loop at all.

The exact recommended cadence will be tightened in a follow-up once the backend confirms a target; treat the ranges above as defensible defaults today.

## Independent verification

Every transfer that reaches `processing` or beyond carries enough information to verify against the network directly:

- **`signature`** — the Solana transaction signature; paste into [Solscan](https://solscan.io/) or [Solana Explorer](https://explorer.solana.com/) to see the on-chain record.
- **`slot`** — the slot the transaction landed in.
- **`blockTime`** — ISO 8601 timestamp of slot finality.
- **`fee`** — fee paid, in lamports.

If you maintain your own indexer (`getSignatureStatuses` against a Solana RPC), cross-reference SDP's `signature` to your indexer's view to confirm SDP's status reflects what your nodes see.

## Risk metadata

When a risk-screening provider is configured on the organization, transfers may carry a `risk` field with `provider`, `score`, `level` (`low | medium | high | unknown`), and `evaluatedAt`. Use this to gate downstream side effects on a risk threshold; the field is absent when no risk provider is wired up.

Risk evaluation is not blocking by default — a transfer can complete with a high-risk score. If you want a transfer to halt on risk threshold, enforce that in your application logic on top of the `risk.level` value.

## Failure handling

When `status` settles to `failed`:

- Read the `error` field. SDP returns the chain-level error message; common causes are insufficient source funds, account frozen by token authority, or stale blockhash.
- Decide whether to retry. The original transfer record is terminal; a retry creates a new transfer with a new ID and (eventually) a new signature.
- Surface the failure to whoever needs to know — the merchant, the customer, your operations team.

## Related

- [Accept overview](/docs/payments/accept-overview) — the broader inbound-payment flow.
- [Indexing and reconciliation](/docs/payments/accept-indexing) — list-based status checking across many transfers.
- [Concepts: the transfer data model](/docs/payments/concepts#the-transfer-data-model) — full field reference.

---

### Indexing and reconciliation
Source: https://platform.solana.com/docs/payments/accept-indexing

> List inbound transfers, paginate, dedupe, and reconcile against your product ledger.

`GET /v1/payments/transfers` is the workhorse for reconciliation. It returns transfers across your organization's custody wallets with filters for direction, status, token, wallet, and date range, paginated. Use it to maintain a local mirror of SDP transfers, to match inbound payments to orders, and to drive your delta-polling worker.

## Listing inbound transfers

The minimal inbound-only request:

<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl "https://api.solana.com/v1/payments/transfers?direction=inbound&pageSize=50" \
  -H "Authorization: Bearer sk_test_..."
```
</Tab>
<Tab value="TypeScript">
```typescript
const url = new URL("https://api.solana.com/v1/payments/transfers");
url.searchParams.set("direction", "inbound");
url.searchParams.set("pageSize", "50");

const res = await fetch(url, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const { data, meta } = await res.json();
// data: Transfer[]
// meta: { total, page, pageSize, hasMore, requestId }
```
</Tab>
</Tabs>

### Query parameters

| Parameter | Type | Notes |
| --- | --- | --- |
| `direction` | `inbound` \| `outbound` | Common filter. Inbound = payments to wallets you control. |
| `status` | `pending` \| `processing` \| `confirmed` \| `finalized` \| `failed` | Scope to a single state. |
| `wallet` | string | SDP custody wallet ID. |
| `walletAddress` | string | Solana address (alternative to `wallet`). |
| `token` | string | Filter by token symbol or on-chain mint (exact match against the stored transfer's `token` value). |
| `from`, `to` | ISO 8601 datetime | Time-window filter on `createdAt`. **Fully honored only on the org-scoped path (no `wallet`/`walletAddress` filter).** On the wallet-scoped path the window is honored for the DB-side branch (`pending`/`processing`/`failed`), but the signature-history branch — which fetches recent on-chain signatures and returns the matching DB rows (which can be `confirmed` or `finalized` once the tracker has updated them) plus synthesized rows for signatures with no DB record (always `confirmed`, or `failed` if the on-chain transaction errored — never `finalized`) — replays the last ~200 signatures and ignores the window. For time-windowed reconciliation, prefer org-scoped. Use with offset (`2026-05-14T00:00:00Z`). |
| `page` | integer | Default `1`. |
| `pageSize` | integer | Default `20`, max `100`. |

## Deduplication

Two natural dedup keys:

1. **`Transfer.id`** — SDP-internal, immutable, present on every transfer (including `pending` ones with no signature yet). Use this as your local primary key.
2. **`Transfer.signature`** — the on-chain signature, populated once the transaction has been built and observed on-chain (typically at `confirmed`/`finalized`, but treat its presence rather than the status enum as the trigger — `processing` and even `pending` records can still have a null signature). **`UNIQUE` across SDP's transfers table** wherever it is set; two records cannot share a signature. Use this if you need to dedupe across multiple data sources (SDP + a parallel chain indexer, say) — only once `signature` is present.

In practice: store transfers in your reconciliation table keyed by SDP `Transfer.id`, and assert `signature` uniqueness once it is populated.

## Reconciling an order

Customer-initiated inbound transfers do **not** populate `Transfer.memo` (memo-program instructions aren't extracted into the field), and the inbound record doesn't expose a reference. The reliable on-record correlation key is the inbound transfer's `destination` — match it against an order whose pre-issued receiving address is that same `destination`.

The typical inbound match cycle:

1. **Read** the recent inbound transfers for the relevant token, using the **org-scoped path** (no `wallet`/`walletAddress` filter) so `from`/`to` are honored:
   ```
   GET /v1/payments/transfers
     ?direction=inbound
     &token=<mint or SOL>
     &from=<since last reconciled>
     &to=<now>
     &pageSize=100
   ```
2. **For each transfer**, look up an open order keyed by the transfer's `destination` (`findOrderByDestination(tr.destination)`).
3. **If matched**, assert the amount and token match the expected values on the order row, then mark the order paid, record the SDP `Transfer.id`, and dispatch downstream side effects (email, shipment, ledger entry).
4. **If unmatched**, leave the transfer for the next pass (it may be an early-arrival for an order you haven't staged yet) or flag it for manual review after a grace window.

If you need memo- or Solana-Pay-`reference`-based correlation (e.g., a single-wallet flow where the destination address is shared across orders), read the on-chain transaction directly via Solana RPC using the transfer's `signature`. SDP does not surface either field on inbound transfer records today.

## Solana Pay reference accounts (roadmap)

The `Prepare` transfer endpoint accepts a `referenceAddress` field — the intent is that you pass a Solana Pay reference pubkey and SDP attaches it on-chain as an account meta, then surfaces it back on the inbound transfer record so you can match payments to orders without trusting the sender's metadata. **This is not wired up today**: the prepare handler accepts the field but does not yet attach it to the transaction, and inbound transfer records do not expose a reference. Until it ships, the realistic correlation options are: (a) **per-order destination addresses** — pre-issue a fresh receiving wallet per order and match by `Transfer.destination` (the recommended pattern; see [Reconciling an order](#reconciling-an-order) above); (b) **on-chain reads via Solana RPC** keyed off the transfer's `signature`, used to recover the memo-program instruction or Solana Pay `reference` that SDP itself doesn't surface for inbound transfers.

## A delta-poll worker

A reconciliation worker that wakes on a tick and asks "what is new since last time":

<Tabs items={["TypeScript"]}>
<Tab value="TypeScript">
```typescript
const OVERLAP_MS = 5_000; // re-query the last 5s on each tick to catch boundary arrivals

async function reconcileTick(state: WorkerState) {
  // Nudge `from` backwards by the overlap window; `alreadySeen` dedupes the
  // resulting duplicates. Always advance the high-water mark to `to` regardless,
  // otherwise the window grows without bound.
  const fromMs = state.lastSeenIso
    ? Date.parse(state.lastSeenIso) - OVERLAP_MS
    : Date.now() - 60_000;
  const from = new Date(fromMs).toISOString();
  const to = new Date().toISOString();

  // Use the org-scoped listing (no wallet filter) so `from`/`to` are honored;
  // see the Query parameters table above.
  for await (const tr of listInbound({ from, to })) {
    if (await alreadySeen(tr.id)) continue;

    // Match by destination — the on-record correlation key for inbound transfers.
    // `tr.memo` is omitted (undefined) for customer-initiated inbound payments.
    const order = await findOrderByDestination(tr.destination);
    if (!order) {
      await persistUnmatched(tr);
      continue;
    }

    if (tr.status === "finalized") {
      await markOrderPaid(order, tr);
    }
    await persistSeen(tr.id, tr.status);
  }

  state.lastSeenIso = to;
}
```
</Tab>
</Tabs>

Notes:

- The overlap window (5s above) re-queries the trailing edge of the last tick to catch transfers that arrive at the boundary; `alreadySeen` collapses the resulting duplicates.
- The high-water mark advances to `to` on every pass even though `from` is rolled back — otherwise the query window grows unbounded.
- Always re-fetch a transfer to upgrade its status: a transfer seen as `confirmed` in one tick will appear as `finalized` in a later tick.
- Use `pageSize=100` and the async iterator pattern (see [Payouts and disbursements](/docs/payments/send-payouts)) to handle large windows.

## Related

- [Verifying a payment](/docs/payments/accept-verification) — single-transfer status reads.
- [Accept overview](/docs/payments/accept-overview) — the broader inbound flow.
- [Payouts and disbursements](/docs/payments/send-payouts) — the same list endpoint used outbound.
- [Payments API reference](/docs/reference/api/payments) — full endpoint reference.

---

## Payments — Ramps

### Ramps
Source: https://platform.solana.com/docs/payments/ramps

> Fiat on-ramps and off-ramps — let users buy crypto with fiat or cash out from a wallet through SDP.

SDP wraps several ramp providers behind two endpoints: `POST /v1/payments/ramps/onramp/execute` (fiat → crypto) and `POST /v1/payments/ramps/offramp/execute` (crypto → fiat). Each call returns a provider-hosted redirect URL that you hand the end-user; the provider handles KYC, payment-method capture, and settlement. SDP does not persist a ramp record or expose a status read endpoint — log the returned `ramp.id` for your own records and observe settlement either provider-side (webhook / their dashboard) or via the resulting on-chain transfer.

For per-provider configuration (credentials, sandbox vs production), see [Ramp providers](/docs/payments/ramps-providers).

## Onramp flow

The end-user journey:

1. Your backend calls `POST /v1/payments/ramps/onramp/execute` with the destination wallet, crypto token, and fiat amount.
2. SDP returns a `redirectUrl` from the chosen provider.
3. You redirect the user (or open the URL in an in-app browser).
4. The provider runs KYC and accepts the user's payment instrument.
5. The provider settles the resulting crypto to the destination wallet on Solana.
6. Your backend detects the resulting inbound transfer via [`GET /v1/payments/transfers?direction=inbound`](/docs/payments/accept-indexing) and/or watches the provider-side status (webhook or read endpoint). SDP does not expose a status read endpoint for the ramp execution itself.

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/payments/ramps/onramp/execute \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "moonpay",
    "destinationWallet": "wal_...",
    "cryptoToken": "USDC",
    "fiatAmount": "100.00",
    "kycReference": "user_4837",
    "redirectUrl": "https://app.example.com/onramp/complete"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/ramps/onramp/execute",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      provider: "moonpay",
      destinationWallet: "wal_...",
      cryptoToken: "USDC",
      fiatAmount: "100.00",
      kycReference: "user_4837",
      redirectUrl: "https://app.example.com/onramp/complete",
    }),
  }
);
const { data } = await response.json();
// data.ramp.redirectUrl — open this in the user's browser
// data.ramp.id — log this for your own records; SDP does not expose a read endpoint to look it up
```
</Tab>
<Tab value="Java">
```java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/ramps/onramp/execute"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "provider": "moonpay",
          "destinationWallet": "wal_...",
          "cryptoToken": "USDC",
          "fiatAmount": "100.00",
          "kycReference": "user_4837",
          "redirectUrl": "https://app.example.com/onramp/complete"
        }"""))
    .build();
```
</Tab>
</Tabs>

### Request fields

| Field | Required | Notes |
| --- | --- | --- |
| `provider` | yes | One of `moonpay`, `lightspark`, `bvnk`. |
| `destinationWallet` | yes | All providers require it. Provider-dependent format: **MoonPay** accepts an SDP wallet ID or a Solana address; **Lightspark** expects a Lightspark account identifier (`ExternalAccount:…`) or a Solana address (a fresh external account is created when a Solana address is passed); **BVNK** resolves the value to a Solana on-chain address and uses it as the payout destination (`payOutDetails.address`). The separate `BVNK_WALLET_ID` env var configures BVNK's *settlement* wallet on the provider side and is unrelated to this field. |
| `cryptoToken` | yes | Token symbol (`USDC`, `USDT`, etc.). Alphanumeric and underscore. |
| `fiatAmount` | yes | Decimal string greater than zero (e.g. `"100.00"`). |
| `fiatCurrency` | no | Currently `USD` only. |
| `kycReference` | conditional | Up to 128 chars; identifies the end-user across your KYC system. **Required for `lightspark` and `bvnk` on-ramp** (carries the Lightspark or BVNK customer id); optional for `moonpay`. |
| `redirectUrl` | no | Valid URL the provider sends the user to on completion. |
| `bvnkCompliance` | no | BVNK-only on the on-ramp side: object of the form `{ "partyDetails": [...] }` carrying compliance party records. Optional for BVNK on-ramp, **required for BVNK off-ramp** (see the offramp table below). Omit the field entirely for `moonpay` and `lightspark`. See [Ramp providers](/docs/payments/ramps-providers#bvnk). |

## Offramp flow

The mirror image — convert from crypto to fiat from a wallet you control:

<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl -X POST https://api.solana.com/v1/payments/ramps/offramp/execute \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "moonpay",
    "sourceWallet": "wal_...",
    "cryptoToken": "USDC",
    "cryptoAmount": "100.00",
    "redirectUrl": "https://app.example.com/offramp/complete"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
  "https://api.solana.com/v1/payments/ramps/offramp/execute",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      provider: "moonpay",
      sourceWallet: "wal_...",
      cryptoToken: "USDC",
      cryptoAmount: "100.00",
      redirectUrl: "https://app.example.com/offramp/complete",
    }),
  }
);
const { data } = await response.json();
// data.ramp.redirectUrl, data.ramp.id — same envelope as onramp
```
</Tab>
</Tabs>

Offramp uses `sourceWallet` (the wallet you're cashing out from) and `cryptoAmount` (the crypto amount to convert). Other fields mirror the onramp request, with two provider-specific differences:

- **`kycReference`** is required for `lightspark` and `bvnk` off-ramp (carries the Lightspark or BVNK customer id; Lightspark uses it as the destination account identifier) and optional for `moonpay`.
- **`bvnkCompliance`** is **required** for BVNK off-ramp: the server validates that `partyDetails` contains at least one entry and rejects the request otherwise. Omit it for `moonpay` and `lightspark`.

## Status field

The response's `ramp.status` is one of `pending`, `processing`, `completed`, `failed`. It reflects the provider's reported state **at the moment the execute call returns** — SDP does not subsequently poll the provider or update the field, and there is no read endpoint to refresh it. For the on-chain side of a successful onramp, the recommended observation is the resulting **inbound transfer** record on the destination custody wallet, surfaced via the [Accept payments](/docs/payments/accept-overview) flow.

## Provider selection

You always pass `provider` explicitly today; SDP does not auto-select. Choose based on:

- **Coverage** — which providers your org has configured (see [Ramp providers](/docs/payments/ramps-providers#provider-gating)).
- **Region** — providers have different country / payment-method support.
- **Sandbox needs** — all three providers ship sandboxes; sandbox vs production is chosen per request from the calling API key's environment (see [Sandbox vs production](/docs/payments/ramps-providers#sandbox-vs-production-selection)).
- **Compliance attachment** — only BVNK accepts the `bvnkCompliance` field (a `{ "partyDetails": [...] }` object) today.

## Fiat currency support

SDP currently accepts `USD` only on the `fiatCurrency` field. Multi-currency support depends on individual provider capabilities and is not exposed via this endpoint today.

## Related

- [Ramp providers](/docs/payments/ramps-providers) — per-provider configuration and capability matrix.
- [Accept overview](/docs/payments/accept-overview) — how onramp deliveries show up as inbound transfers.
- [Provider onboarding](/docs/reference/provider-onboarding) — how providers are activated for an organization.

---

### Ramp providers
Source: https://platform.solana.com/docs/payments/ramps-providers

> MoonPay, Lightspark, and BVNK — capability matrix, configuration, and per-provider notes.

SDP currently integrates three ramp providers. They are configured at the organization level via environment variables on the SDP API deployment; the same `provider` string on `POST /v1/payments/ramps/onramp/execute` and `POST /v1/payments/ramps/offramp/execute` selects between them.

## Sandbox vs production selection

The sandbox/production mode is chosen **per request**, not at deployment time:

- Non-production SDP deployments (`ENVIRONMENT !== "production"`) always use sandbox credentials.
- In production deployments, the calling API key's environment picks the mode — `sk_test_` keys use sandbox, `sk_live_` use production.

Configure both credential pairs for any provider you want to expose to test and live traffic on the same deployment.

## Capability matrix

| Provider | Onramp | Offramp | Sandbox | Compliance attachment | Notes |
| --- | --- | --- | --- | --- | --- |
| MoonPay | ✓ | ✓ | ✓ | — | Sandbox vs production is selected per request (see [Sandbox vs production](#sandbox-vs-production-selection)). |
| Lightspark | ✓ | ✓ | ✓ | — | Grid API. Same per-request sandbox/production selection as MoonPay. |
| BVNK | ✓ | ✓ | ✓ | ✓ (`bvnkCompliance`) | Hawk auth. |

## MoonPay

| Variable | Required | Notes |
| --- | --- | --- |
| `MOONPAY_API_KEY` | conditional | Production API key. Required when the request resolves to production. |
| `MOONPAY_SECRET_KEY` | conditional | Production secret used to sign onramp / offramp URLs. Paired with `MOONPAY_API_KEY`. |
| `MOONPAY_SANDBOX_API_KEY` | conditional | Sandbox API key. Required when the request resolves to sandbox. |
| `MOONPAY_SANDBOX_SECRET_KEY` | conditional | Sandbox secret. Paired with `MOONPAY_SANDBOX_API_KEY`. |
| `MOONPAY_ONRAMP_URL` | no | Override the onramp host. Defaults to MoonPay's standard URLs; the sandbox/production choice follows the [per-request rule](#sandbox-vs-production-selection). |
| `MOONPAY_OFFRAMP_URL` | no | Same idea for offramp. |

MoonPay normalizes token symbols internally — `USDC` is mapped to `usdc_sol`, `USDT` to `usdt_sol` — so you pass plain symbols on the request and SDP handles the mapping.

## Lightspark

| Variable | Required | Notes |
| --- | --- | --- |
| `LIGHTSPARK_GRID_CLIENT_ID` | conditional | Production Grid API client identifier. Required when the request resolves to production. |
| `LIGHTSPARK_GRID_CLIENT_SECRET` | conditional | Production Grid API client secret. |
| `LIGHTSPARK_GRID_SANDBOX_CLIENT_ID` | conditional | Sandbox Grid API client identifier. Required when the request resolves to sandbox. |
| `LIGHTSPARK_GRID_SANDBOX_CLIENT_SECRET` | conditional | Sandbox Grid API client secret. |

Lightspark is integrated against the Grid API (`https://api.lightspark.com/grid/2025-10-13`); the sandbox/production choice follows the [per-request rule](#sandbox-vs-production-selection). Lightspark identifies wallets by its own `ExternalAccount:…` ids — onramp `destinationWallet` accepts that id *or* a Solana wallet address (SDP creates the external account if needed), while offramp `sourceWallet` requires the Lightspark account id.

## BVNK

| Variable | Required | Notes |
| --- | --- | --- |
| `BVNK_WALLET_ID` | conditional | Production BVNK-side wallet used for settlement. |
| `BVNK_HAWK_AUTH_ID` | conditional | Production Hawk auth id. Paired with `BVNK_HAWK_SECRET_KEY`. |
| `BVNK_HAWK_SECRET_KEY` | conditional | Production Hawk secret key. |
| `BVNK_SANDBOX_WALLET_ID` | conditional | Sandbox BVNK wallet. Required when the request resolves to sandbox. |
| `BVNK_SANDBOX_HAWK_AUTH_ID` | conditional | Sandbox Hawk auth id. |
| `BVNK_SANDBOX_HAWK_SECRET_KEY` | conditional | Sandbox Hawk secret key. |
| `BVNK_API_BASE_URL` | no | Override the default BVNK host. Defaults to `https://api.bvnk.com` for production and `https://api.sandbox.bvnk.com` for sandbox; the sandbox/production choice follows the [per-request rule](#sandbox-vs-production-selection). |

BVNK uses Hawk authentication. Bearer-token auth is not currently supported.

BVNK is the only provider that currently accepts the `bvnkCompliance` field on the ramp request — an object of the form `{ "partyDetails": [...] }` carrying compliance party records used to satisfy travel-rule and KYC sharing requirements. The exact shape of each party record is provider-defined; see BVNK's compliance docs for the field schema.

```json
{
  "provider": "bvnk",
  "destinationWallet": "wal_...",
  "cryptoToken": "USDC",
  "fiatAmount": "100.00",
  "bvnkCompliance": {
    "partyDetails": [
      { "type": "individual", "firstName": "...", "lastName": "...", "...": "..." }
    ]
  }
}
```

## Provider gating

Even when all three providers' env vars are populated, an org can be restricted to a subset. SDP gates by `assertProviderAvailable` on every ramp call — if the org's entitlement does not include the requested provider, the call returns an error. To activate or change provider entitlements for a deployment, see [Provider onboarding](/docs/reference/provider-onboarding).

## Choosing a provider

| Need | Lean towards |
| --- | --- |
| Fastest sandbox iteration | MoonPay or BVNK. |
| Travel-rule / compliance party metadata | BVNK. |
| Lightning-aware fiat settlement | Lightspark. |
| Broadest payment-method coverage | MoonPay (region-dependent). |

For self-hosted deployments where only a subset of providers will ever be configured, see [Providers optional for self-hosted SDP](/docs/reference/provider-onboarding) — SDP supports running with any single provider active.

## Related

- [Ramps](/docs/payments/ramps) — onramp and offramp endpoints and request shape.
- [Provider onboarding](/docs/reference/provider-onboarding) — activation flow.

---

## Tutorials

### Issue a Regulated Stablecoin
Source: https://platform.solana.com/docs/tutorials/issue-a-regulated-stablecoin

> Build a GENIUS-compliant digital dollar on Solana with institutional custody, integrated compliance, and operational reversibility from day one.

This tutorial walks you through issuing a regulated stablecoin on Solana. The product you ship at the end is one that a Federal Reserve examiner could read and a bank's compliance committee could sign against. Issuing one is not the same as deploying a token. It requires institutional custody, integrated compliance, and operational reversibility from day one. The [GENIUS Act](https://www.gibsondunn.com/the-genius-act-a-new-era-of-stablecoin-regulation/) treats those capabilities as preconditions, not features.

## 1. What this work demands

**1. Custody is not your laptop.** A regulated stablecoin is held by a federally chartered custodian. For a US issuer operating under the GENIUS Act framework, that usually means Anchorage Digital Bank or an equivalent federally chartered crypto bank. The custodian holds the signing keys for the mint, freeze, and permanent-delegate authorities. Not the engineer at the issuer.

**2. Compliance is integrated from day one.** Every transfer destination gets screened before the transfer is built. Every frozen account ties back to a court order, sanctions notice, or internal compliance ticket. The integration is not bolted on later; it sits on the same API surface as the mint and burn endpoints.

**3. Operational reversibility is the product, not a failure mode.** Freeze, seize, and force-burn are the technological capabilities the GENIUS Act requires you to have ready before launch. They exist in the SDP API because the regulation says they must, and your compliance officer will check for them. Section 5 covers each in detail.

**4. The reader is institutional.** This tutorial assumes you are a developer at a financial institution or fintech, building under a compliance officer's oversight. The decisions you make are decisions she will see in product review. Section 2 walks through the prerequisites in that light.

Every code sample in this tutorial was exercised against SDP's sandbox during writing; the request and response shapes shown here are what the live API actually returned.

## 2. Prerequisites

You need an SDP account, a custody provider, two project-scoped API keys (one for routine operations, one for compliance operations), and a funded devnet wallet. About thirty minutes end-to-end if you have nothing today.

### 1. Account and organization

Sign up at [platform.solana.com](https://platform.solana.com) using email, Google, or GitHub. Create your organization when the dashboard prompts you. SDP provisions a matching organization on its side via a [Clerk](https://clerk.com) webhook; the dashboard's onboarding cards clear once the sync lands.

### 2. Custody provider

An institutional issuer in the US pipeline configures [Anchorage Digital Bank](https://www.anchorage.com/platform/stablecoin-issuance) as the custodian. The Anchorage onboarding team provisions the signing wallets and binds them to your SDP organization. If you're working on the self-serve sandbox without an Anchorage relationship yet, SDP auto-provisions Privy wallets for development access instead. The API surface is identical; only the wallet record's `provider` field differs.

### 3. API keys: the two-key institutional pattern

The hardest cliff in setup is that the dashboard's "Create API key" dialog only mints **org-scoped** keys. The issuance API requires **project-scoped** keys. Project management in the dashboard is on the SDP roadmap; until it lands, the workaround is a short sequence of API calls.

The bootstrap path:

<Steps>

<Step>

From the dashboard, mint an org-scoped API key with **Role: Admin**, **Environment: Sandbox**. This is your bootstrap credential.

![Create API key dialog in the SDP dashboard, with Role set to Admin and Environment set to Sandbox.](/issue-a-regulated-stablecoin/01-create-api-key-dialog.png)

</Step>

<Step>

With the Admin key, call `GET /v1/projects` and pick the project with slug `default-sandbox`. Every organization is provisioned with exactly one sandbox and one production project automatically — isolation between workloads comes from separate organizations, not additional projects. The project ID is what you'll need in the next step:

```json
{
  "data": {
    "projects": [
      {
        "id": "prj_3bc13bcb-...",
        "slug": "default-sandbox",
        "environment": "sandbox",
        "status": "active"
      },
      {
        "id": "prj_8a201d47-...",
        "slug": "default-production",
        "environment": "production",
        "status": "active"
      }
    ]
  }
}
```

</Step>

<Step>

Call `POST /v1/projects/{projectId}/api-keys` twice to mint two project-scoped keys: one with `role: "api_developer"` for routine create, mint, and burn calls, and one with `role: "api_admin"` for compliance operations (screening, freeze, seize, force-burn). Each response includes the full key value exactly once:

```json
{
  "data": {
    "apiKey": {
      "id": "key_375fe1e1-...",
      "keyPrefix": "sk_test__b3",
      "key": "sk_test_<full key, shown once>",
      "role": "api_developer",
      "environment": "sandbox"
    }
  }
}
```

<Callout type="warn">
The `key` field appears only on this response. Capture it into your secrets manager immediately; SDP does not surface it again.
</Callout>

</Step>

</Steps>

Store the two project-scoped keys with your other production secrets. Keep the org-scoped Admin key cold; you only touch it when you provision additional keys or rotate.

### 4. Sandbox readers: fund your custody wallet

On devnet, the custody wallet starts at zero lamports, which means SDP cannot pay transaction fees from it. Fund it via the browser faucet:

<Steps>

<Step>
Go to [faucet.solana.com](https://faucet.solana.com).
</Step>

<Step>
Set the network to **Devnet**.
</Step>

<Step>
Paste the wallet's public key.
</Step>

<Step>
Request one SOL.
</Step>

</Steps>

The browser faucet is more reliable than the RPC `requestAirdrop` method, which is heavily rate-limited. Production readers operating under Anchorage do not handle this step manually; the bank funds the custody address as part of its standard operational support.

### 5. API base URL

The canonical SDP API URL is `https://api.solana.com`. Use this hostname for every production request.

### 6. Wire up your client and verify

Wire two HTTP clients (one per project-scoped key) using a thin wrapper around `fetch`:

```javascript
const BASE = "https://api.solana.com";

class SdpClient {
  constructor(apiKey, baseUrl = BASE) {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async request(method, path, body) {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    const payload = await res.json();
    if (!res.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
    return payload;
  }

  get(path) { return this.request("GET", path); }
  post(path, body) { return this.request("POST", path, body); }
}

const sdp = new SdpClient(process.env.SDP_API_KEY);
const sdpAdmin = new SdpClient(process.env.SDP_ADMIN_API_KEY);
```

Use `sdp` for routine operations (create, mint, burn) and `sdpAdmin` for compliance operations (screening, freeze, seize, force-burn). Section 4 extends this client with `Idempotency-Key` header support when state-mutating execute endpoints come into play.

Now confirm the setup by listing the wallets bound to your project:

```javascript
const { data } = await sdp.get("/v1/wallets");
console.log(`Custody wallets configured: ${data.wallets.length}`);
```

A working setup prints:

```
Custody wallets configured: 2
```

If the response shows at least one wallet, your custody is bound and you are ready to deploy a token. If it returns an empty array, your custody provider has not finished provisioning. For the Anchorage pipeline, contact your onboarding lead. For the self-serve sandbox, wait a minute and re-run; Privy's initial wallet provision can lag the SDP organization creation by a few seconds.

## 3. Design the token

Before any API call, you decide three things: which template to start from, what compliance posture the token deploys with, and which authorities the custodian holds. The design is what your compliance officer reviews. The API calls in Section 4 are the mechanical follow-through.

### 1. Pick the template

SDP ships three public token templates, each a preset of [Token-2022](https://solana.com/docs/tokens/extensions) extensions tuned to an institutional product shape:

- **`stablecoin`**. 6 decimals by default. Required extensions: `permanentDelegate` and `pausable`. Allowlist off. The right template for a regulated dollar.
- **`tokenized-security`**. 8 decimals. Required extensions: `permanentDelegate`, `pausable`, and `scaledUiAmount`. Allowlist on; accounts open frozen. The right template for a regulated security.
- **`custom`**. 9 decimals. No required extensions. Use only when neither template above fits.

This tutorial uses `stablecoin`. The required extensions are what a US permitted payment stablecoin issuer needs to satisfy the GENIUS Act's technological-capability mandate covered in Section 5.

### 2. Set the compliance posture

Four boolean flags on token creation determine the posture you deploy with:

- **`isFreezable`**. Set to `true`. If `false` at create time, the token deploys with no freeze authority and freezing becomes impossible afterward.
- **`isMintable`**. Set to `true` for an active stablecoin. You will mint as new fiat reserves arrive at the custody bank.
- **`requiresAllowlist`**. Leave off for a payment stablecoin. The institutional pattern is screen-before-transfer, not gate-by-allowlist. The `tokenized-security` template flips this on automatically.
- **`maxSupply`**. Optional cap in UI units (decimal string). Useful for pilot programs with a hard ceiling. Omit for uncapped supply.

### 3. Plan the authorities

A regulated stablecoin has four on-chain authorities, each a distinct role on the mint. At deploy, SDP wires all four to your custody signer by default:

- **`mint`**. Authorizes new supply.
- **`freeze`**. Freezes and unfreezes individual token accounts.
- **`permanentDelegate`**. Moves or burns any holder's tokens without their signature. The technological capability the GENIUS Act requires for lawful orders.
- **`metadata`**. Updates on-chain token metadata.

A fifth, the **pause authority**, lives on the `pausable` extension configuration. SDP defaults it to the `mint` authority if you don't set it explicitly.

You can delegate or revoke any of these later via `POST /v1/issuance/tokens/{id}/authority`. Default-everything-to-custody is the institutional baseline.

### 4. Capture the design as a config

Section 4 takes one input: the configuration object representing the design choices above. For the Treasury Pilot USD example this tutorial uses end-to-end:

```javascript
const tokenConfig = {
  template: "stablecoin",
  name: "Treasury Pilot USD",
  symbol: "TPUSD",
  decimals: 6,
  description: "GENIUS-compliant treasury pilot stablecoin.",
  maxSupply: "100000000",
  isFreezable: true,
  isMintable: true,
};
```

With your design captured, you're ready to make the first state-mutating call.

## 4. Create and deploy

Three API calls take a design from object literal to a deployed regulated stablecoin on Solana: one to create the record, one to deploy on-chain, and an optional `/prepare` call to diagnose if either fails opaquely. All three run from the Developer key (`sdp`).

### 1. Extend the client for Idempotency-Key

Every state-mutating execute endpoint in SDP (deploy, mint, burn, seize, force-burn, authority) supports an `Idempotency-Key` HTTP header. A retry with the same key replays the original response. A retry with a different key is a new request. The header is how you make network-level retries safe under partial failure. Replace the `SdpClient` definition from Section 2 with this version:

```javascript
class SdpClient {
  constructor(apiKey, baseUrl = BASE) {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async request(method, path, body, { idempotencyKey } = {}) {
    const headers = {
      Authorization: `Bearer ${this.apiKey}`,
      "Content-Type": "application/json",
    };
    if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

    const res = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });
    const payload = await res.json();
    if (!res.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
    return payload;
  }

  get(path) { return this.request("GET", path); }
  post(path, body, opts) { return this.request("POST", path, body, opts); }
}

const sdp = new SdpClient(process.env.SDP_API_KEY);
const sdpAdmin = new SdpClient(process.env.SDP_ADMIN_API_KEY);
const idempotencyKey = (op) => `${op}-${crypto.randomUUID()}`;
```

Generate one key per logical operation, not per retry. Create the key once before the first attempt, store it in a variable, and reuse that same value for every retry of the same call.

### 2. Create the token record

The first call writes the token's metadata to SDP's database. Nothing happens on Solana yet. The token starts in `status: "pending"` with no `mintAddress`:

```javascript
const created = await sdp.post("/v1/issuance/tokens", tokenConfig);
const tokenId = created.data.token.id;
```

A successful response:

```json
{
  "data": {
    "token": {
      "id": "tok_e152fd38-...",
      "template": "stablecoin",
      "symbol": "TPUSD",
      "decimals": 6,
      "status": "pending",
      "mintAddress": null,
      "isFreezable": true,
      "isMintable": true,
      "requiresAllowlist": false,
      "maxSupply": "100000000"
    }
  }
}
```

Save the `tok_*` ID; the deploy call uses it.

### 3. Deploy on-chain

The deploy call submits a Token-2022 mint creation transaction to Solana via the custody signer, and updates the token record with the resulting on-chain addresses:

```javascript
const deployKey = idempotencyKey("deploy");
const deployed = await sdp.post(
  `/v1/issuance/tokens/${tokenId}/deploy`,
  {},
  { idempotencyKey: deployKey }
);
const mintAddress = deployed.data.token.mintAddress;
```

A successful response carries the deployed token record with `status: "active"` and the on-chain `mintAddress` populated:

```json
{
  "data": {
    "token": {
      "id": "tok_e152fd38-...",
      "status": "active",
      "mintAddress": "6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L",
      "mintAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X",
      "freezeAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X",
      "metadataAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X",
      "extensions": {
        "defaultAccountState": "initialized",
        "permanentDelegate": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X"
      },
      "deployedAt": "2026-05-18T14:50:43.329Z"
    }
  }
}
```

The `mintAuthority`, `freezeAuthority`, and `metadataAuthority` all resolve to the custody wallet address that SDP picked from your configured custody (the project signing wallet, falling back to the org signing wallet). The `permanentDelegate` is set at the same time and appears on the token record's `extensions.permanentDelegate` field rather than as a top-level authority.

### 4. When deploy fails opaquely, fall back to /prepare

<Callout type="warn">
Deploy can return a bare `500 INTERNAL_ERROR` with no `details` field. The most common cause on devnet is an unfunded custody wallet (Section 2 covered the airdrop fix), but any downstream Solana RPC failure surfaces this way. The SDP team is closing this gap so deploy will return the underlying Solana error directly; until that ships, the diagnostic move is to call the `/prepare` variant of the same endpoint.
</Callout>

```javascript
const prepared = await sdp.post(
  `/v1/issuance/tokens/${tokenId}/deploy/prepare`,
  {}
);
console.log(prepared.data.simulation);
```

`/prepare` builds the transaction and runs simulation against Solana without submitting. Its response surfaces the real error in `simulation.error`:

```json
{
  "data": {
    "preparedTransaction": { "serialized": "...", "blockhash": "..." },
    "simulation": {
      "success": false,
      "error": "\"AccountNotFound\""
    }
  }
}
```

`"AccountNotFound"` on the fee payer means the custody wallet has zero SOL. Fund it via the faucet (Section 2, subsection 4) and re-run `/deploy` with the same `Idempotency-Key` if you want to replay, or a fresh key if you want a new attempt. SDP treats different keys as different requests; same key with the same params replays the prior result.

This prepare-as-diagnostic pattern works for every endpoint that has a `/prepare` variant: mint, burn, seize, force-burn, authority. Whenever an execute call fails opaquely, the corresponding `/prepare` surfaces the underlying Solana error.

### 5. Confirm the deploy

Before moving to Section 5, confirm the on-chain state matches the record:

```javascript
const { data } = await sdp.get(`/v1/issuance/tokens/${tokenId}`);
console.log(`Status: ${data.token.status}`);
console.log(`Mint:   ${data.token.mintAddress}`);
```

A working deploy prints:

```
Status: active
Mint:   6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L
```

The same state surfaces in the SDP dashboard's issuance view:

![Deployed token shown in the SDP dashboard's issuance page, with status active and the mint address surfaced.](/issue-a-regulated-stablecoin/02-deployed-token-dashboard.png)

With the token deployed, Section 5 configures the compliance controls the GENIUS Act requires before any tokens move.

## 5. Configure compliance controls

Every other section of this tutorial is engineering. This one is the regulatory backbone.

Section 4(a)(5) of the GENIUS Act treats a permitted payment stablecoin issuer as a financial institution for purposes of the Bank Secrecy Act. That single sentence is what makes everything below load-bearing. The Act also requires you to have "[the technological capability to comply with all lawful orders to seize, freeze, burn or prevent the transfer of outstanding stablecoins](https://www.skadden.com/insights/publications/2025/07/us-establishes-first-federal-regulatory-framework)." The endpoints in this section map directly onto that statutory phrase, in the order you'll encounter them: screening first, freeze for per-account blocks, seize for compliance recovery, force-burn as the final remedy.

### Screen before you move

Every transfer destination and every allowlist entry gets screened before your application acts on it. One API call, one response.

```javascript
const response = await sdpAdmin.post("/v1/compliance/address-screenings", {
  address: destinationAddress,
  network: "solana",
  intent: "transfer_destination",
});

for (const provider of response.data.screening.providers) {
  if (provider.status === "error" || provider.riskScore > THRESHOLD) {
    throw new TransferBlocked(provider);
  }
}
```

SDP integrates with four screening providers: Range, Elliptic, TRM, and Chainalysis. One call fans out to all configured providers and returns a per-provider array. The pattern to write against is provider-agnostic: iterate over `providers[]`, fail closed on any `status: "error"` or threshold breach. The institutional reality is that a single provider can be unavailable for credential or rate-limit reasons; a fail-closed policy keeps you defensible even when one feed degrades.

On the sandbox response this tutorial cites, Range and Elliptic return numeric scores, TRM returns a healthy status without scoring a fresh address, and Chainalysis returns an upstream credential error. Treat each provider's verdict as one signal; your policy should not depend on any single provider's availability.

### Freeze when a specific account needs to stop

Freezing halts one holder's account without touching any other holder. The freeze is on-chain, enforced by the Token-2022 program. SDP signs the freeze instruction with the freeze authority that was set at deploy; the on-chain program does the enforcement.

```javascript
const holderFreezeKey = idempotencyKey("freeze");
await sdpAdmin.post(
  `/v1/issuance/tokens/${tokenId}/freeze`,
  {
    accountAddress: holderAddress,
    reason: "Court order #2026-IL-0142",
  },
  { idempotencyKey: holderFreezeKey }
);
```

You pass the holder's wallet address; SDP derives the [Associated Token Account](https://solana.com/docs/tokens/basics/create-token-account) (the on-chain account that holds that holder's TPUSD balance) and freezes it. Once frozen, transfers to or from that account fail at the program level, not at SDP's gateway.

<Callout type="info">
Section 7 demonstrates the resulting error: an HTTP 502 `SOLANA_RPC_ERROR` from SDP that wraps the Token-2022 program error `custom program error: 0x11`, which is the program's `AccountFrozen` signal. The compliance verdict is in the wrapped message; the outer SDP code is upstream-call diagnostic.
</Callout>

### Seize for compliance recovery

Seize moves tokens out of a holder's account without their signature, via the **permanent delegate** authority set at deploy. The permanent delegate is a Token-2022 extension that grants a named authority the right to transfer or burn any holder's tokens regardless of holder signature. This is the technological capability the GENIUS Act requires you to have ready for lawful orders. The institutional use case is a court-ordered transfer or a sanctions matter where you must recover specific tokens to a controlled wallet.

```javascript
const seizeKey = idempotencyKey("seize");
await sdpAdmin.post(
  `/v1/issuance/tokens/${tokenId}/seize`,
  {
    seize: {
      source: holderAddress,
      destination: custodyAddress,
      amount: "50000.00",
      memo: "OFAC matter 2026-04-018",
    },
  },
  { idempotencyKey: seizeKey }
);
```

The memo persists on the transaction record. It's the audit attribution your compliance officer reads months later when she reconciles the action against the court order or sanctions notice that authorized it.

### Force-burn as the final remedy

Force-burn destroys tokens at a holder's account. Same authority as seize, different effect. Reach for it only when the tokens cannot be recovered to a controlled wallet, for example a self-custodied address whose key you cannot obtain.

```javascript
const forceBurnKey = idempotencyKey("force-burn");
await sdpAdmin.post(
  `/v1/issuance/tokens/${tokenId}/force-burn`,
  {
    forceBurn: {
      source: holderAddress,
      amount: "12000.00",
      memo: "regulator-ordered burn 2026-04-021",
    },
  },
  { idempotencyKey: forceBurnKey }
);
```

### The institutional pattern that holds this together

Every endpoint in this section requires the `tokens:admin` permission. That is by design. Keep two project-scoped API keys:

- **Developer key**: routine create, mint, and burn calls.
- **Admin key**: screen, freeze, seize, and force-burn.

The compliance officer reviewing this design sees a clean separation: routine issuance cannot escalate into compliance actions without a deliberate credential change. Every call writes an entry through SDP's audit service. That trail is the artifact a regulator will eventually ask for, and it is not opt-in.

## 6. Mint the first tokens

Minting is how new supply enters circulation. For a regulated stablecoin, every mint corresponds to a fiat reserve event: dollars arriving at the custody bank, attested by the reserve custodian, and recorded against an issuance ticket. The mint call is small. What you put in the memo field is what your compliance officer reconciles against the reserve report.

### 1. Send the mint call

Minting requires the `tokens:write` permission, so the Developer key (`sdp`) is the right credential:

```javascript
const mintKey = idempotencyKey("mint");
const minted = await sdp.post(
  `/v1/issuance/tokens/${tokenId}/mint`,
  {
    mint: {
      destination: custodyAddress,
      amount: "1000000",
      memo: "Reserve mint Q2-2026, reference: ANB-RESV-2026-04-091",
    },
  },
  { idempotencyKey: mintKey }
);
```

Three field rules to keep clear:

- **`destination`** is a Solana base58 address. For the initial issuer-holds-supply pattern, this is your custody wallet's public key. For subsequent mints to client wallets, it's the recipient's wallet.
- **`amount`** is a decimal string in UI units (post-decimals). Not a BigInt, not a number. `"1000000"` mints one million TPUSD because the token has 6 decimals.
- **`memo`** is up to 100 characters. SDP stores it on the transaction record. This is the institutional audit attribution: the reference your compliance officer will use months later when reconciling the mint against the reserve custodian's monthly report.

### 2. Read the response

A successful mint returns the transaction record and the token account that received the tokens:

```json
{
  "data": {
    "transaction": {
      "id": "ttx_88240bf5-...",
      "type": "mint",
      "status": "confirmed",
      "signature": "51XZb4Tqm1G4LmSPUwnLrxENWmmhW9UE15LhxrwMrS6vKL4avFzKBBBcPNEQuiMEVgxWRgbdjJERo6K4taQEM9XG",
      "params": {
        "destination": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X",
        "amount": "1000000",
        "memo": "Reserve mint Q2-2026, reference: ANB-RESV-2026-04-091",
        "tokenAccount": "NLcEWVJ8oiHdbf6xuSLSAfKfJ1Gqcw4ksYHnkCyEy3t"
      }
    },
    "tokenAccount": "NLcEWVJ8oiHdbf6xuSLSAfKfJ1Gqcw4ksYHnkCyEy3t"
  }
}
```

The `tokenAccount` is the Associated Token Account that holds the newly minted tokens. SDP creates it on first mint to that destination if it doesn't already exist; subsequent mints to the same destination reuse it.

### 3. The reserve attestation tie-in

<Callout type="info">
The GENIUS Act requires a permitted payment stablecoin issuer to publish a monthly public attestation of reserve composition, signed by a registered public accounting firm, with separate CEO/CFO certification. The mint memo is what closes the loop on the engineering side of that obligation.
</Callout>

Every mint memo should carry, at minimum, a reserve reference number that maps to a specific reserve event in the custodian's records. Two minimum-viable patterns:

- **Single-deposit mint**: `"ANB-RESV-2026-04-091"`. The deposit reference the custody bank assigned when the fiat reserves were received.
- **Batched mint against a reserve commitment**: `"Q2-2026 commitment tranche 3 of 6"`. A pre-arranged reserve commitment broken into scheduled releases.

The pattern that does not work is mint memos that don't reconcile cleanly to a reserve record. The compliance officer reading the attestation report needs a one-to-one mapping; the auditor signing the report depends on it.

### 4. Confirm the supply

```javascript
const { data } = await sdp.get(`/v1/issuance/tokens/${tokenId}`);
console.log(`Total supply: ${data.token.totalSupply}`);
```

Prints:

```
Total supply: 1000000
```

With tokens in circulation, Section 7 puts the compliance controls from Section 5 to a live test.

## 7. Test a transfer and a compliance action

Section 5's compliance controls only matter if they enforce. This section moves real tokens between two wallets, then puts the freeze authority to a live test. The block is enforced on-chain by Token-2022, not by SDP's gateway. That distinction is the institutionally important detail and the reason the error envelope reads the way it does.

### 1. The clean transfer

Transfers run on the `/v1/payments/transfers` endpoint family, separate from the issuance family that Section 4 used. The required permissions are `payments:write` and `wallets:read`, both of which the Developer key (`sdp`) carries:

```javascript
const { data } = await sdp.get("/v1/wallets");
const sourceWalletId = data.wallets[0].walletId;

const screening = await sdpAdmin.post("/v1/compliance/address-screenings", {
  address: destinationAddress,
  network: "solana",
  intent: "transfer_destination",
});

for (const provider of screening.data.screening.providers) {
  if (provider.status === "error" || provider.riskScore > THRESHOLD) {
    throw new TransferBlocked(provider);
  }
}

const transfer = await sdp.post("/v1/payments/transfers", {
  source: sourceWalletId,
  destination: destinationAddress,
  token: mintAddress,
  amount: "100",
  memo: "Treasury Pilot internal transfer #042",
});
```

Three field rules to keep clear:

- **`source`** is the wallet's provider-specific `walletId` (for example `privy_jfxdbsq4bg6dxzsd7imgon3j`). It is not the wallet's public key, and not the SDP custody-wallet identifier (`cwlt_*`). The handler does exact-match on `walletId`; passing the public key returns `404 NOT_FOUND`.
- **`token`** is the on-chain mint address you got from Section 4's deploy response. It is not the SDP token ID (`tok_*`).
- **`destination`** is any valid Solana base58 address. It does not have to be a wallet SDP knows about.

A successful response includes the on-chain signature once the transaction confirms:

```json
{
  "data": {
    "transfer": {
      "id": "xfr_f570debc-...",
      "type": "transfer",
      "direction": "outbound",
      "status": "confirmed",
      "signature": "ekNe4HK67AXptsx6krYATZd5xYeq8wYQShmN9oHUdAipVJVFELGwiLhdJNCAbyN2TPv9SwzBTNz6igoAfWTdCYP",
      "source": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X",
      "destination": "2H2w3hSxAPTybuTL7LkhPbyk6bWYi94x2J4aqBhhyJL2",
      "token": "6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L",
      "amount": "100",
      "memo": "Treasury Pilot internal transfer #042"
    }
  }
}
```

### 2. Freeze the destination

To demonstrate the on-chain block, freeze the destination's TPUSD account using the Admin key. Freeze requires `tokens:admin`, which the Developer key does not carry:

```javascript
const transferFreezeKey = idempotencyKey("freeze");
await sdpAdmin.post(
  `/v1/issuance/tokens/${tokenId}/freeze`,
  {
    accountAddress: destinationAddress,
    reason: "Compliance hold pending OFAC review 2026-04-019",
  },
  { idempotencyKey: transferFreezeKey }
);
```

The freeze records a frozen account in SDP's audit trail and applies the Token-2022 freeze instruction to the destination's Associated Token Account on-chain. The on-chain side is verifiable on Solana Explorer:

![Freeze transaction on Solana Explorer in devnet, showing the Token-2022 FreezeAccount instruction applied to the destination's associated token account.](/issue-a-regulated-stablecoin/03-freeze-on-chain-explorer.png)

### 3. Attempt the transfer again

Run the same transfer call. This time it fails:

```javascript
try {
  await sdp.post("/v1/payments/transfers", {
    source: sourceWalletId,
    destination: destinationAddress,
    token: mintAddress,
    amount: "100",
    memo: "Compliance test, expecting block",
  });
} catch (error) {
  // handle the on-chain block (see step 4)
}
```

The error envelope SDP returns:

```json
{
  "error": {
    "code": "SOLANA_RPC_ERROR",
    "message": "Failed to sign and send transaction: RPC Error -32000: Invalid transaction: Transaction simulation failed: Error processing Instruction 1: custom program error: 0x11"
  }
}
```

The outer error code is `SOLANA_RPC_ERROR`, but it is upstream-call diagnostic, not the compliance verdict. The actual reason lives in the wrapped message: `custom program error: 0x11`. That is Token-2022's `AccountFrozen` signal. The block was enforced on-chain by the program, not by SDP's gateway.

### 4. Parse the wrapped error

<Callout type="warn">
The institutional pattern is to write your error handler against the wrapped on-chain code rather than the outer SDP code. The outer code tells you something went wrong upstream; the wrapped code tells you what.
</Callout>

```javascript
function isOnChainCompliance(error) {
  return error.message?.includes("custom program error: 0x11");
}

try {
  // transfer attempt
} catch (error) {
  if (isOnChainCompliance(error)) {
    // log to the compliance system, do not retry
    return reportToCompliance(error);
  }
  // anything else: treat as a transient RPC failure and apply your retry policy
  throw error;
}
```

Token-2022's `0x11` is `AccountFrozen` (decimal 17). Treat it as a compliance verdict, not a retry-able failure. The same pattern (outer code is the family of failure, wrapped code is the actual reason) applies whenever SDP wraps a downstream Solana RPC error. Section 4's prepare-as-diagnostic pattern handles the inverse case: when the outer error is opaque and the wrapped message is empty, `/prepare` surfaces the simulation error directly.

### 5. Cleanup

For a production environment, unfreezing happens only after the underlying compliance review resolves. For sandbox testing, you can unfreeze immediately:

```javascript
await sdpAdmin.post(`/v1/issuance/tokens/${tokenId}/unfreeze`, {
  accountAddress: destinationAddress,
});
```

With the freeze cleared, the destination can receive transfers again. Section 8 covers the production transition: what changes between `sk_test_` and `sk_live_`, and which checks your compliance officer signs against before this same code runs on mainnet.

## 8. Production transition checklist

The work between `sk_test_` and `sk_live_` is not a flag flip. It is the moment your compliance officer signs against the runbook your engineering team has been building since Section 2. Going to production means three things change mechanically and a longer list of things your institution attests to.

### 1. What changes mechanically

Your sandbox setup is built around six artifacts that all swap for production equivalents:

- **API keys**. Replace your project-scoped Developer and Admin keys with `sk_live_` keys minted from your production project. Use the Section 2 bootstrap pattern with a fresh Admin key, selecting the `default-production` project from `GET /v1/projects` instead of `default-sandbox`, and fresh project-scoped keys minted under it.
- **Custody provider**. Production reads `provider` as your federally chartered custodian (Anchorage Digital Bank for most US institutional issuers), bound to your organization through the Anchorage onboarding flow. The Privy sandbox wallets do not carry over.
- **Solana network**. Mainnet-beta replaces devnet. The custody address gets funded by your custody bank as part of standard operational support, not by a faucet.
- **API base URL**. `https://api.solana.com` is the canonical production base.
- **Screening providers**. Confirm the production provider mix matches your compliance team's policy. Run a `POST /v1/compliance/address-screenings` against a known address to verify each provider returns `status: "ok"` before live traffic.
- **Idempotency-Key strategy**. In production, generate idempotency keys deterministically per operation (for example, `mint-${reserveDepositReference}`) rather than purely random per attempt. This lets your runbook resend safely without minting twice.

### 2. What your compliance officer signs off on

Before any `sk_live_` key sees real traffic, the compliance officer needs evidence that the institutional posture from Section 1 is operational, not aspirational. The minimum sign-off list:

- **GENIUS Section 4(a)(5) capabilities demonstrated**. Screening, freeze, seize, and force-burn each exercised on sandbox with a captured response in the audit trail.
- **BSA/AML program documented**. Written risk-based policy with internal controls, ongoing customer due diligence, independent testing, and a US-located AML/CFT compliance officer named (proposed 31 CFR § 1033.210 under the FinCEN/OFAC implementing rules).
- **Sanctions program ready**. OFAC screening provider configured and tested, technical capability to block transactions in place, secondary-market screening procedure defined.
- **Reserve attestation cadence agreed**. Registered public accounting firm engaged, monthly examination schedule signed, CEO/CFO certification process defined.
- **Audit trail review**. SDP's audit-service entries reviewed for completeness; mint memos from Section 6 reconcile cleanly to reserve references in the custody bank's records.
- **Incident response runbook**. The operational playbook for a freeze, seize, or force-burn order. Court order arrives, who signs in to what credential, how long the chain of custody runs, where the documentation lands.

### 3. The cutover

When the sign-off is in, the cutover itself is small. Repeat the Section 2 bootstrap with `environment: "production"`, mint your production keys, store them with your other live secrets, and update your runtime environment. Run the Section 2 wallet verification call against the production API base to confirm the wallets are bound. The first production mint corresponds to a real fiat reserve event at your custody bank.

## 9. What's next

You have a deployed regulated stablecoin, the compliance controls configured, and a production transition checklist your compliance officer can sign against. What you have built is the issuance side of the institutional product. Two adjacent tutorials carry forward from here.

<Cards>
  <Card title="Tokenize a Treasury Fund">
    The design and issuance pattern for a tokenized money market fund or treasury bond product under the `tokenized-security` template. Compliance posture differs from the stablecoin pattern in three ways: allowlist on by default, accounts open frozen, and an additional `scaledUiAmount` extension for accrued yield. Most of the Section 5 controls work identically; the differences sit in Sections 3 and 4.
  </Card>
  <Card title="Run Enterprise Stablecoin Payments">
    Picks up after Section 7. Covers transfer rails for institutional payment flows: bulk transfers, scheduled payments, on-ramp and off-ramp integration with custody bank settlement, and the policy-engine patterns that gate transfers at scale.
  </Card>
</Cards>

### Reference docs

For deeper API surface beyond what this tutorial covers:

- [SDP API reference](https://platform.solana.com/docs/reference/api). The generated endpoint inventory across all public API families.
- [SDP Postman collection](https://platform.solana.com/docs/reference/postman-collection). Importable request bundle for the full API.
- [Token-2022 program documentation](https://solana.com/docs/tokens/extensions). The on-chain program SDP wraps.
- [GENIUS Act analysis](https://www.gibsondunn.com/the-genius-act-a-new-era-of-stablecoin-regulation/). Statutory interpretation covering each subsection of the Act.
- [FinCEN/OFAC implementing rules NPRM](https://www.federalregister.gov/documents/2026/04/10/2026-06963/permitted-payment-stablecoin-issuer-anti-money-launderingcountering-the-financing-of-terrorism). The current implementing-rule source for the BSA designation in GENIUS Section 4(a)(5).

---

### End-to-end Payment Flow
Source: https://platform.solana.com/docs/tutorials/end-to-end-payment-flow

> A high-level payment flow from quote to settlement.

This walkthrough covers a typical payment integration lifecycle:

1. Create or resolve the funding wallet.
2. Build and validate transfer intent (amount, destination, token).
3. Choose signing mode:
- custody execution (server signs and submits)
- prepare mode (client signs)
4. Execute the transfer and capture identifiers for observability.
5. Query transfer status until finalization.
6. Reconcile with your product ledger and user-facing state.

Use the [Payments API reference](/docs/reference/api/payments), [Wallets API reference](/docs/reference/api/wallets), and [Prepare vs Execute](/docs/guides/prepare-vs-execute) guide for endpoint-level request and response details.

---

## Self-Hosting

### Self-Hosting
Source: https://platform.solana.com/docs/self-hosting

> Run the Solana Developer Platform on your own infrastructure with Docker Compose.

Self-hosting runs the full Solana Developer Platform — API, web dashboard, docs,
database, and cache — on infrastructure you control, from prebuilt container
images. You bring your own authentication, Solana RPC, and signing provider; SDP
stays the same product you'd use as a managed service.

This section covers installing the stack, generating its configuration, and
keeping it running.

## What you run

`infra/self-hosted/compose.yml` starts six services:

| Service | Image | Purpose |
|---|---|---|
| `sdp-api` | `sdp-api` | The core API (default port `8787`). |
| `sdp-web` | `sdp-web` | The dashboard web app (default port `3000`). |
| `sdp-docs` | `sdp-docs` | This documentation site (default port `3001`). |
| `sdp-migrate` | `sdp-api` | Runs database migrations once on startup, then exits. |
| `postgres` | `postgres:16-alpine` | Bundled database (or point at an external one). |
| `redis` | `redis:7-alpine` | Bundled cache (or point at an external one). |

## Start here

- **[Quickstart](/docs/self-hosting/quickstart)** — install the stack and bring it
  up for the first time.
- **[First devnet deployment](/docs/self-hosting/first-devnet-deployment)** —
  log into the dashboard, initialize a local wallet, and verify a wallet-scoped
  API key.
- **[Configurator](/docs/self-hosting/configurator)** — generate a complete `.env`
  in your browser or terminal.
- **[Environment reference](/docs/self-hosting/env-reference)** — what every
  configuration variable does and which ones are required.

## Operate

- **[Upgrade & backup](/docs/self-hosting/upgrade-and-backup)** — move to a new
  release and protect your data.
- **[Troubleshooting](/docs/self-hosting/troubleshooting)** — common startup and
  configuration failures.
- **[External co-signers](/docs/self-hosting/external-co-signers)** — host
  provider-specific signing automation alongside the stack.

## Requirements

- A 64-bit Linux or macOS host with **Docker Engine** and **Docker Compose v2**.
- Outbound network access to pull images and reach your Solana RPC and
  authentication providers.
- A [Clerk](https://clerk.com) application for authentication and a Solana RPC
  endpoint. See the [environment reference](/docs/self-hosting/env-reference) for
  the full list.

---

### Quickstart
Source: https://platform.solana.com/docs/self-hosting/quickstart

> Install the self-hosted stack, generate a .env, and bring it up with Docker Compose.

This guide takes you from an empty host to a running Solana Developer Platform.
It assumes **Docker Engine** and **Docker Compose v2** are installed and the
Docker daemon is running.

## 1. Install

The install script downloads `compose.yml` and `.env.example` for the latest
release into `~/sdp` and verifies them against the release checksums.

```bash
curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | bash
```

The script is open source — you can read it before running it, and verify the
checksums and signature out of band. The commented header of `install.sh`
documents the full verified-install flow and the environment variables you can
override (`SDP_INSTALL_DIR`, `INSTALL_VERSION`, and others).

Prefer to do it by hand? Download `compose.yml` and `.env.example` from the
[latest release](https://github.com/solana-foundation/solana-developer-platform/releases/latest)
into a working directory instead.

## 2. Generate your `.env`

The stack reads its configuration from a `.env` file next to `compose.yml`. The
[configurator](/docs/self-hosting/configurator) fills it in for you, generates
the app secrets, and validates your answers.

- **In your browser** — open the [configurator](/docs/self-hosting/configurator),
  answer the prompts, and download the `.env` into `~/sdp`.
- **In your terminal** — run the configurator from the API image:

  ```bash
  docker run --rm -it -v "$HOME/sdp:/out" \
    ghcr.io/solana-foundation/sdp/sdp-api:latest \
    node configure.js --out /out/.env
  ```

  Use the same release tag you installed in place of `latest` so the configurator
  matches your `compose.yml`.

See the [environment reference](/docs/self-hosting/env-reference) for what each
variable means and which are required.

## 3. Bring up the stack

```bash
cd ~/sdp
docker compose up -d
```

On startup, `sdp-migrate` runs the database migrations once, then the API, web,
and docs services start. The first run pulls the images, so it may take a few
minutes.

## 4. Verify

```bash
docker compose ps
docker compose logs -f sdp-api
```

When the services report healthy, the dashboard is at `http://localhost:3000`,
the API at `http://localhost:8787`, and these docs at `http://localhost:3001`.
The API exposes a [health endpoint](/docs/reference/api/health) you can poll from
a load balancer or uptime check.

If a service fails to start or a required variable is missing, see
[Troubleshooting](/docs/self-hosting/troubleshooting).

## Next steps

- [First devnet deployment](/docs/self-hosting/first-devnet-deployment) — log
  into the dashboard, initialize a local wallet, and verify API access.
- [Upgrade & backup](/docs/self-hosting/upgrade-and-backup) — keep the stack
  current and protect your data.
- [Environment reference](/docs/self-hosting/env-reference) — tune ports, RPC,
  signing, and authentication.

---

### First Devnet Deployment
Source: https://platform.solana.com/docs/self-hosting/first-devnet-deployment

> Run self-hosted SDP on devnet, log into the dashboard, initialize a local wallet, and verify API access.

This tutorial takes a fresh self-hosted install from healthy containers to the
first useful devnet workflow: dashboard login, local wallet initialization,
wallet-scoped API key creation, and an authenticated API call.

Use this after the [Quickstart](/docs/self-hosting/quickstart). The path here is
devnet-only and uses the default local signer. Provider-specific signing setup
belongs in a later hardening pass.

<Callout type="warn">
The public Solana devnet RPC endpoint is useful for smoke tests, but it can be
rate-limited or incomplete for balance-heavy wallet views. Use a dedicated
devnet RPC endpoint from your provider before treating this flow as reliable.
</Callout>

<Steps>

<Step>

## Confirm the stack is healthy

From the directory where the installer placed `compose.yml` and `.env`:

```bash
cd ~/sdp
docker compose up -d
docker compose ps
curl http://localhost:8787/health
```

The health call should return a JSON response with `status: "ok"`. The dashboard
is at `http://localhost:3000`, the API is at `http://localhost:8787`, and the
self-hosted docs are at `http://localhost:3001`.

</Step>

<Step>

## Configure Clerk for dashboard login

In your Clerk development app:

- Copy the publishable key, secret key, and issuer into the
  [configurator](/docs/self-hosting/configurator).
- Customize the session token (Clerk dashboard → **Sessions** → **Customize
  session token**) with the claims from the
  [Clerk setup guide](https://github.com/solana-foundation/solana-developer-platform/blob/main/apps/sdp-api/docs/self-hosting/clerk-setup.md).
- Expose your local API with the webhook tunnel: reserve a free stable domain
  at [dashboard.ngrok.com/domains](https://dashboard.ngrok.com/domains), set it
  as `WEBHOOK_INGEST_DOMAIN` in `apps/sdp-api/.env.local`, and run
  `pnpm dev:webhooks`.
- Create a webhook endpoint that points at
  `https://<your-domain>/webhooks/clerk/link-orgs` — Clerk delivers over the
  public internet and cannot reach `localhost` directly.
- Copy the webhook signing secret into `CLERK_WEBHOOK_SECRET`.

`CLERK_WEBHOOK_SECRET` is optional at raw env-validation level, but this tutorial
requires it because the webhook is what creates the local SDP organization record
after dashboard signup.

</Step>

<Step>

## Generate the devnet `.env`

Open the [configurator](/docs/self-hosting/configurator), keep the defaults for
the first deployment, and fill in:

- `SOLANA_RPC_URL` with a devnet RPC endpoint.
- Clerk publishable key, secret key, issuer, and webhook secret.
- Local signing with native fee payment.

The signing step generates a local Solana signer in the browser and shows the
public key to fund on devnet. Save the generated file as `~/sdp/.env`, then
restart the stack:

```bash
cd ~/sdp
docker compose up -d
```

<Callout type="info">
In self-hosted mode, SDP entitles every provider that is configured in the
environment. Providers that are not configured remain unavailable, so the default
devnet path exposes only the local signer.
</Callout>

</Step>

<Step>

## Log into the dashboard

Open `http://localhost:3000` and sign in through Clerk. Create or join an
organization when prompted. After the Clerk webhook lands, the dashboard should
show the organization and project context instead of onboarding errors.

If the dashboard loads but organization state does not appear, check the API logs:

```bash
docker compose logs -f sdp-api
```

Look for Clerk webhook errors, session token claim mismatches, or missing
`CLERK_WEBHOOK_SECRET`.

</Step>

<Step>

## Initialize a local custody wallet

In the dashboard, open **Wallets**, choose the local provider, and create a wallet
named `Devnet local wallet`.

If you prefer the API path, use an admin key from the dashboard:

```bash
curl -X POST http://localhost:8787/v1/wallets/initialize \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "local",
    "walletLabel": "Devnet local wallet"
  }'
```

Copy the returned `walletId`. You will bind the API key to this wallet in the
next step.

</Step>

<Step>

## Create a wallet-scoped API key

Open **API keys** in the dashboard, create a sandbox key, and choose **Selected
wallets**. Select the local wallet you just created and make it the default
signing wallet.

Save the full key when the dashboard shows it. SDP only displays it once.

</Step>

<Step>

## Verify the key can read wallets

Use the new key against your self-hosted API:

```bash
export SDP_API_KEY="sk_test_..."

curl "http://localhost:8787/v1/wallets?view=summary" \
  -H "Authorization: Bearer $SDP_API_KEY"
```

The response should include the local wallet you created. At this point you have
a self-hosted devnet stack with dashboard auth, local custody signing, and an API
key bound to the wallet it is allowed to use.

</Step>

</Steps>

## Next steps

- Use [Environment reference](/docs/self-hosting/env-reference) to move from
  localhost defaults to real domains, external Postgres, or external Redis.
- Use [Upgrade & backup](/docs/self-hosting/upgrade-and-backup) before keeping
  meaningful operator data in the stack.
- Use [External co-signers](/docs/self-hosting/external-co-signers) when a
  provider requires an additional signing process outside the default compose
  stack.

---

### Configurator
Source: https://platform.solana.com/docs/self-hosting/configurator

> Generate a complete .env for a self-hosted Solana Developer Platform, entirely in your browser.

Use the guided steps below to generate a ready-to-use `.env` for the default
devnet self-hosting path. Everything runs in your browser — nothing is sent
anywhere. Drop the file next to `compose.yml`, then run `docker compose up -d`.

For the full operator walkthrough after download, continue with
[First devnet deployment](/docs/self-hosting/first-devnet-deployment).

<EnvConfigurator />

---

### Environment Reference
Source: https://platform.solana.com/docs/self-hosting/env-reference

> The configuration variables a self-hosted Solana Developer Platform reads from .env.

The self-hosted stack reads its configuration from a single `.env` file next to
`compose.yml`. The [configurator](/docs/self-hosting/configurator) is the
authoritative source of the field list, defaults, and validation — it always
matches the release you installed. This page explains how the configuration is
organized and the variables you are most likely to set by hand.

The configurator groups variables into sections:

| Section | What it covers |
|---|---|
| Basic | Core runtime — environment, deployment mode, Transactional Email sender and Resend key. |
| Database | Bundled Postgres or an external database. |
| Cache | Bundled Redis or an external cache. |
| Solana RPC | Network and RPC endpoint. |
| Authentication (Clerk) | Required Clerk keys and JWT settings. |
| Signing provider | Which signer(s) to enable and their credentials. |
| Fee payment | How transaction fees are paid. |
| Secrets | App secrets, generated locally. |
| Advanced | Image source, ports, and internal service URLs. |

## Required variables

These must be set or the stack will refuse to start:

| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Password for the bundled Postgres. Always required — the bundled `postgres` service starts even when the API points at an external `DATABASE_URL`. |
| `API_KEY_PEPPER` | Server-side pepper for API key hashing. Generate with `openssl rand -hex 32`. |
| `CUSTODY_ENCRYPTION_KEY` | Encryption key for custody material at rest. Generate with `openssl rand -base64 32`. |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key for the web app. |
| `CLERK_SECRET_KEY` | Clerk secret key for the API. |
| `CLERK_ISSUER` | Clerk issuer URL used to validate tokens. |
| `SOLANA_RPC_URL` | Solana RPC endpoint the API calls. |
| `SIGNING_PROVIDER` | The default signing provider. Each non-local provider adds its own required credentials. |

The configurator generates `API_KEY_PEPPER` and `CUSTODY_ENCRYPTION_KEY` for you
and can auto-generate `POSTGRES_PASSWORD`.

## Transactional Email

SDP-owned Transactional Email uses Resend. Set `EMAIL_FROM` to the sender address
and `RESEND_API_KEY` to the Resend API key before enabling product flows that send
email. Organization member invitation emails are handled by Clerk and do not use
these settings.

## Database

In the configurator, `DATABASE_MODE` chooses between the bundled Postgres and an
external database. It is a generation-time selector and is not written to `.env`.

For the bundled database, set `POSTGRES_DB`, `POSTGRES_USER`, and
`POSTGRES_PASSWORD`; the API and migration services build their connection string
from them. To use an external database, set `DATABASE_URL` to your own
`postgresql://…` string and the API and migrations connect there instead. Note
that `compose.yml` always starts the bundled `postgres` container — with an
external `DATABASE_URL` it simply goes unused, and `POSTGRES_PASSWORD` is still
required for it to start.

The configurator hides `POSTGRES_PASSWORD` once you select an external database
but still emits an auto-generated value for it, since the bundled `postgres`
container needs one to start even when nothing connects to it.

## Cache

Like `DATABASE_MODE`, `CACHE_MODE` is a configurator-only selector and is not
written to `.env`. To use an external cache, set `REDIS_URL` to your own
`redis://…` endpoint; the bundled `redis` container still starts but goes unused.

## Signing providers

`SIGNING_PROVIDER` is the default signer written to `.env`. Supported values are
`local`, `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, and `utila`.
The configurator also tracks a `SIGNING_PROVIDERS` selection to decide which
fields to show, but that key is configurator-only and is not written to `.env`.
Each non-local provider requires its own credentials — for example Fireblocks
needs an API key, secret, and vault ID — and the configurator only prompts for
the providers you enable.

For provider-managed co-signer runtimes that live outside the default stack, see
[External co-signers](/docs/self-hosting/external-co-signers).

## Advanced

| Variable | Default | Description |
|---|---|---|
| `SDP_IMAGE_REGISTRY` | `ghcr.io/solana-foundation/sdp` | Registry the service images are pulled from. |
| `SDP_VERSION` | `latest` | Image tag for every SDP service. Pin this to a release tag in production. |
| `SDP_API_PORT` | `8787` | Host port for the API. |
| `SDP_WEB_PORT` | `3000` | Host port for the dashboard. |
| `SDP_DOCS_PORT` | `3001` | Host port for these docs. |

`SDP_API_BASE_URL` defaults to the in-network service name `http://sdp-api:8787`
for server-to-server calls. The browser-facing `NEXT_PUBLIC_*` URLs default to
`localhost` (for example `http://localhost:8787` and `http://localhost:3000`) and
need changing when you put the services behind your own domains or ingress.

---

### Upgrade & Backup
Source: https://platform.solana.com/docs/self-hosting/upgrade-and-backup

> Move a self-hosted Solana Developer Platform to a new release and protect its data.

This page covers upgrading to a new release and backing up the data that matters
on a self-hosted stack.

## Upgrade

A release ships new container images plus an updated `compose.yml`. To upgrade:

1. **Refresh the install files.** Re-run the install script. It replaces
   `compose.yml` with the new release's version (verified against the release
   checksums) and leaves any existing `.env` and `.env.example` in place, printing
   a link to the new release's `.env.example` so you can check for new variables.

   ```bash
   curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | bash
   ```

2. **Pin the version.** Set `SDP_VERSION` in `.env` to the release tag you are
   upgrading to (rather than `latest`) so every service runs the same, known
   build.

3. **Pull and restart.**

   ```bash
   cd ~/sdp
   docker compose pull
   docker compose up -d
   ```

   `sdp-migrate` runs any new database migrations once before the API starts.

4. **Verify** the services are healthy, as in the
   [Quickstart](/docs/self-hosting/quickstart#4-verify).

To upgrade to a specific release instead of the latest, set `INSTALL_VERSION`
when running the script and the matching `SDP_VERSION` in `.env`.

### Roll back

Set `SDP_VERSION` back to the previous tag and run `docker compose up -d`. Note
that migrations are not automatically reversed; if a release applied a migration,
restore from a backup taken before the upgrade rather than relying on a
version downgrade alone.

## Backup

Two things hold your state:

- **The Postgres database** — all platform data. With the bundled database it
  lives in the `sdp-postgres-data` Docker volume; with an external `DATABASE_URL`,
  back it up with your database provider's tooling instead.
- **Your `.env`** — secrets and configuration. Store it somewhere safe (a secret
  manager); it cannot be regenerated identically.

Redis is a cache and does not need backing up.

### Back up the database

Take a logical dump with `pg_dump` from the running container. Running it through
`sh -c` lets `pg_dump` read `POSTGRES_USER` and `POSTGRES_DB` from inside the
container, so custom values are honored:

```bash
cd ~/sdp
docker compose exec -T postgres \
  sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > sdp-backup.sql
```

Keep these dumps off-host and on a schedule that matches your recovery needs.

### Restore the database

A plain `pg_dump` includes the full schema and the migration history, so restore
into an **empty** database and let the stack start on top of it. Do not restore
over an already-migrated database, or the dump's schema collides with the one
`sdp-migrate` created on the previous `up`.

```bash
cd ~/sdp
docker compose down                       # stop every service
docker volume rm sdp_sdp-postgres-data    # discard the current database (destructive)
docker compose up -d postgres             # start a fresh, empty Postgres
until docker compose exec -T postgres \
  sh -c 'pg_isready -U "$POSTGRES_USER"' >/dev/null 2>&1; do sleep 1; done
docker compose exec -T postgres \
  sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < sdp-backup.sql
docker compose up -d                      # start the rest
```

On that final `up`, `sdp-migrate` skips every migration already recorded in the
restored dump. Restoring a dump taken at the same release leaves it with nothing
to do; restoring an older dump before an upgrade lets it apply only the newer
migrations.

Restore the matching `.env` alongside it so secrets such as
`CUSTODY_ENCRYPTION_KEY` line up with the data they protect. For an external
`DATABASE_URL`, restore through your database provider rather than the bundled
`postgres` service.

---

### Troubleshooting
Source: https://platform.solana.com/docs/self-hosting/troubleshooting

> Diagnose common startup and configuration failures on a self-hosted stack.

Most self-hosting problems surface at startup. Start by looking at the service
state and logs:

```bash
cd ~/sdp
docker compose ps
docker compose logs -f sdp-api
```

## Docker is not ready

The install script checks for Docker up front. If you see that Docker is not
installed, that Compose v2 is unavailable, or that the daemon is unreachable,
install or start Docker and retry — see the
[Docker Engine](https://docs.docker.com/engine/install/) and
[Compose](https://docs.docker.com/compose/install/) docs.

## A required variable is missing

`compose.yml` fails fast when a required variable is unset, with a message such
as `set POSTGRES_PASSWORD in .env`. Make sure a `.env` exists next to
`compose.yml` and fills in every required variable. The
[configurator](/docs/self-hosting/configurator) produces a complete file; the
[environment reference](/docs/self-hosting/env-reference) lists what is required.

## Images won't pull

If `docker compose pull` reports access denied or a missing manifest:

- Confirm `SDP_IMAGE_REGISTRY` and `SDP_VERSION` point at a registry and tag you
  can reach.
- Pull a single image directly to see the underlying error, e.g.
  `docker pull ghcr.io/solana-foundation/sdp/sdp-api:latest`.
- If the registry is private, run `docker login` for it first.

## Port already in use

The API, web, and docs default to ports `8787`, `3000`, and `3001`. If another
process holds one of them, the service won't bind. Override the host port in
`.env` with `SDP_API_PORT`, `SDP_WEB_PORT`, or `SDP_DOCS_PORT` and run
`docker compose up -d` again.

## Migrations failed

`sdp-migrate` runs once before the API starts; if it fails, the API stays down.
Inspect it with:

```bash
docker compose logs sdp-migrate
```

A failure here is usually a database connection problem — check `POSTGRES_*` (or
`DATABASE_URL`) and that the `postgres` service is healthy in `docker compose ps`.

## Database connection refused

The API and migration services wait for Postgres to report healthy before
starting. If they still can't connect, verify the credentials in `.env`, and for
an external database (`DATABASE_URL` set) that it is reachable from inside the
Docker network.

## Authentication errors in the dashboard

Sign-in problems point at the Clerk configuration. Re-check
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, and `CLERK_ISSUER`
against your Clerk application, and confirm the keys match the environment
(development vs production) you intend to run.

## Still stuck?

Capture the failing service's logs and your (secret-redacted) `.env` settings,
then open an issue on the
[project repository](https://github.com/solana-foundation/solana-developer-platform).

---

### External Co-Signers
Source: https://platform.solana.com/docs/self-hosting/external-co-signers

> Host provider-specific signing automation outside the default self-hosted SDP stack.

Some signing providers need an operator-managed co-signer or automation process
in addition to the SDP API. Run those processes outside the default self-hosted
SDP stack.

`infra/self-hosted/compose.yml` is intentionally unchanged by default. It starts
the SDP API, web app, docs app, database, cache, and migrations. Provider
co-signers are separate operational services because they usually have their own
secret handling, identity, ingress, and availability requirements.

## Production pattern

Use a managed container service such as Google Cloud Run, or the equivalent in
your cloud, for provider-specific co-signer runtimes.

- Use the provider- or operator-supplied co-signer image and runtime.
- Store co-signer secrets in a cloud secret manager, not in the SDP compose file.
- Run the co-signer with a dedicated service account that can read only the
  secrets it needs.
- Configure ingress and invoker IAM for the provider flow. Prefer private or
  authenticated ingress when the provider does not require a public callback.
- Set `minScale` to `1` when callback latency or provider availability matters;
  use scale-to-zero only when cold starts are acceptable.
- Keep development, staging, and production co-signers separated by provider
  account, cloud project, service account, and secrets.
- Alert on failed signing attempts, container restarts, authentication failures,
  and stale or expired key material.

## Utila readiness

Utila is the motivating future provider for this pattern. When Utila signer
support lands, the co-signer should be hosted as an external operator service,
for example on Cloud Run, while SDP keeps its API, web, docs, database, and cache
inside the normal self-hosted stack.

This page does not make Utila a live SDP signing provider. Do not set
`SIGNING_PROVIDER=utila` until the Utila signer integration explicitly adds that
provider option and the required runtime configuration.

Operator scaffolding for a Utila Cloud Run deployment lives in
`infra/utila/cloud-run/`.

That scaffold is intended to host the co-signer endpoint before Utila becomes a
default self-hosted service. Operators can deploy their own co-signer image,
protect the endpoint with Cloud Run IAM or runtime-level request verification,
and pass the resulting service URL and token into a custom SDP Utila integration.

---

## Reference

### Reference
Source: https://platform.solana.com/docs/reference

> API reference, token types, provider onboarding, and integration resources.

Resources for integrating with SDP: API endpoints, supported token templates, provider onboarding, AI consumption, and the Postman collection.

---

### Issuance Token Types
Source: https://platform.solana.com/docs/reference/issuance-token-types

> How to choose a token template, and the default controls each one applies.

The public issuance surface currently exposes three token templates:

| Template | Defaults | Notes |
| --- | --- | --- |
| `stablecoin` | 6 decimals, permanent delegate + pausable extensions, default account state initialized | Intended for fiat-backed token patterns with compliance controls. |
| `tokenized-security` | 8 decimals, allowlist required, permanent delegate + pausable + scaled UI amount extensions, default account state frozen | Regulated asset flows with stronger defaults. |
| `custom` | 9 decimals default (up to 18 max), extension set is fully configurable | Use when your requirements do not match preset templates. |

These defaults are sourced from the API implementation in `apps/sdp-api/src/services/issuance/templates/definitions.ts`.

## Selecting a template

Each template is a vetted, fixed composition of Token-2022 extensions for a specific class of
asset, rather than an à-la-carte menu. The appropriate template is determined by the nature of
the instrument being issued. The `custom` template applies to assets that do not fit the
stablecoin or tokenized-security models.

### Stablecoin

Intended for tokens pegged to a fiat currency and redeemable at par (1:1). The template applies
the compliance controls typical of regulated fiat tokens: a **pausable** extension (transfers
can be frozen in an emergency) and a **permanent delegate** (tokens can be seized or clawed back
in response to a compliance event). A blocklist applies by default.

Stablecoins cannot pay interest or yield to holders. This reflects payment-stablecoin frameworks
such as the U.S. GENIUS Act and the EU's MiCA, which prohibit issuers from paying interest on the
token itself. Interest- or yield-bearing products are issued as tokenized securities (typically
funds) rather than stablecoins.

### Tokenized security

Intended for regulated instruments — equity, debt, or fund shares. The template applies the
stablecoin controls together with stronger defaults: an **allowlist is required**, new accounts
are **frozen by default** (holders must be approved before transacting), and the **scaled UI
amount** extension is added for corporate actions such as splits or share-class rebasing.
Interest- and yield-bearing instruments (for example debt or fund tokens) also fall under this
template.

### Custom

Corresponds to the **Non-Security Digital Asset** classification in the issuance interface —
assets that are neither fiat stablecoins nor regulated securities, such as commodities, real
estate, or collectibles. The template exposes the full Token-2022 extension set (transfer fees,
transfer hooks, non-transferable behaviour, interest, and others) and applies where the preset
templates do not match the instrument.

### Summary

| Instrument | Template |
| --- | --- |
| Pegged to a fiat currency, redeemable 1:1, with no yield | `stablecoin` |
| Regulated security (equity, debt, or fund), or an instrument that pays a return | `tokenized-security` |
| Any other asset, or one requiring extensions the presets do not offer | `custom` (Non-Security Digital Asset) |

<Callout type="warn">
Token-2022 extensions are set when the mint is created and cannot be changed afterward. The
template selection is therefore permanent for a given token; changing it requires issuing a new
token.
</Callout>

This page provides general guidance and is not legal advice; regulatory treatment depends on the
jurisdiction and on how the instrument is structured.

## Amount format

Mint, burn, seize, and force-burn request bodies accept `amount` as a **decimal string in UI units** — the human-readable token value (for example `"1"` or `"1.5"`). SDP converts to on-chain base units using the token's `decimals`. This differs from wallet balance responses, which return both raw `amount` and `uiAmount`.

---

### Infrastructure Provider Onboarding
Source: https://platform.solana.com/docs/reference/provider-onboarding

> Add an infrastructure provider integration to SDP through the self-service contribution process.

SDP currently supports infrastructure provider integrations across custody, RPC, compliance, and ramps. We will add future integration categories as SDP expands.

Onboarded providers are presented as available integrations, not endorsed or recommended partners. Institutions using SDP are responsible for their own provider due diligence.

## Start here

Select the category that matches your integration, complete the intake form, and review the criteria before opening a pull request.

| Category | Intake form | Criteria |
| --- | --- | --- |
| Custodial enterprise wallet | [Complete custody intake](https://solanafoundation.typeform.com/to/wShiq9SN) | [Download custody criteria PDF](/provider-onboarding/custodial-wallet-vendor.pdf) |
| RPC infrastructure | [Complete RPC intake](https://solanafoundation.typeform.com/to/cq5m65jI) | [Download RPC criteria PDF](/provider-onboarding/rpc-providers.pdf) |
| Ramp | [Complete ramp intake](https://solanafoundation.typeform.com/to/sxTGbwXt) | [Download ramp criteria PDF](/provider-onboarding/ramp-criteria.pdf) |

If your integration is for compliance or another infrastructure category without a published intake, contact the SDP team before opening a pull request.

## Onboarding steps

1. Review the criteria PDF for your category.
2. Complete the matching intake form.
3. Provide sandbox or playground API access.
4. Share documentation, test credentials, supported networks, rate limits, and sandbox limitations.
5. Build the SDP integration with tests and docs.
6. Open a pull request that links to the intake submission or tracking issue.

The SDP team reviews intake completeness within five business days. Gate evaluation usually takes one to three weeks depending on category, documentation completeness, and required technical validation.

## Evaluation

Each category uses the finalized criteria linked above. Every review starts with hard gates, followed by scored evaluation when applicable.

At a high level, SDP evaluates:

- Solana compatibility for the category.
- Stable sandbox or playground access.
- Clear API and integration documentation.
- Predictable error handling and testability.
- Operational readiness and support.
- Required regulatory, compliance, or security posture for the category.

Providers that do not clear the required gates may be tracked for future support, but should not be presented as generally available in SDP until gaps are resolved.

## Access requirements

Provide access suitable for repeatable validation, including:

- Sandbox API keys or equivalent credentials.
- Test accounts, wallets, endpoints, or settlement rails when required.
- Supported Solana networks.
- Known rate limits, feature flags, and sandbox limitations.
- Technical support contact for review.

## Pull request expectations

An SDP infrastructure provider contribution usually includes:

- Provider metadata and capability registration.
- API support for setup, configuration, and category-specific runtime operations.
- Dashboard support where applicable.
- Tests for setup, success paths, failure states, disabled configuration, and missing credentials.
- User-facing docs for setup, sandbox behavior, and provider limits.

Open the pull request after intake is complete, or after the SDP team has created a tracking issue while intake is being finalized.

---

### AI Consumption
Source: https://platform.solana.com/docs/reference/ai-consumption

> Public machine-readable entry points and guidance for agents and AI systems consuming SDP docs and APIs.

Use this page when you need the public AI-facing entry points for Solana Developer Platform.

## Machine-readable resources

- [llms.txt](/docs/ai/llms.txt): Concise discovery file with canonical URLs, supported surfaces, and key starting pages.
- [llms-full.txt](/docs/ai/llms-full.txt): Expanded docs map generated from the docs navigation and public API reference.
- [OpenAPI](/docs/reference/api): Human-readable API reference generated from the public contract.
- [OpenAPI JSON](https://api.solana.com/openapi.json): Machine-readable API contract for the public SDP API.
- [API llms.txt](https://api.solana.com/llms.txt): API-only discovery entry point for endpoint families and authentication expectations.
- [Postman Collection](/docs/reference/postman-collection): Downloadable public collection generated from the same OpenAPI source.

## Recommended ingestion order

1. Start with [llms.txt](/docs/ai/llms.txt).
2. Expand to [llms-full.txt](/docs/ai/llms-full.txt) when you need broader site coverage.
3. Use the [API reference](/docs/reference/api) and [OpenAPI JSON](https://api.solana.com/openapi.json) for exact request and response schemas.
4. Use the product guides for workflow sequencing, operational constraints, and supported patterns.

## Scope

- These AI resources are limited to the supported public SDP surface.
- Hidden or internal-only route families are intentionally excluded.
- Public docs should be treated as the source of truth ahead of internal implementation details.

## Coverage focus

- Wallets and custody
- API key management
- Projects
- Token issuance and lifecycle operations
- Payments, transfers, and ramps
- Compliance screening

---

### Postman Collection
Source: https://platform.solana.com/docs/reference/postman-collection

> Download the public SDP API Postman collection generated from the OpenAPI contract.

The Postman collection is generated from the same public OpenAPI contract that powers the API reference. It includes only end-user-facing API families:

- Health
- API Keys
- Wallets
- Projects
- Issuance
- Payments
- Compliance

Internal-only endpoint families such as `rpc`, `admin`, `onboarding`, `auth`, `organizations`, and `members` are intentionally excluded.

<div>
  <a href="/docs/postman/collection.json" download>Download Postman collection</a>
  {" · "}
  <a href="/docs/postman/collection.json">Open raw JSON</a>
</div>

When you import the collection into Postman, set:

- `baseUrl` to the SDP API environment you want to target
- `sdpApiKey` to your bearer API key

---

## API

### API Reference
Source: https://platform.solana.com/docs/reference/api

> Endpoint index from the repository OpenAPI spec.

<div>
  <a href="/docs/postman/collection.json" download>Download Postman collection</a>
  {" · "}
  <a href="/docs/postman/collection.json">Open raw JSON</a>
</div>

- [Health](/docs/reference/api/health)
- [API Keys](/docs/reference/api/api-keys)
- [Wallets](/docs/reference/api/wallets)
- [Projects](/docs/reference/api/projects)
- [Issuance](/docs/reference/api/issuance)
- [Payments](/docs/reference/api/payments)
- [Policies](/docs/reference/api/policies)
- [Compliance](/docs/reference/api/compliance)
- [Asset Profiles](/docs/reference/api/asset-profiles)

---

### Health
Source: https://platform.solana.com/docs/reference/api/health

> Service health and readiness endpoints.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/health` | Health check |
| `GET` | `/health/ready` | Readiness check |

---

### API Keys
Source: https://platform.solana.com/docs/reference/api/api-keys

> API key management endpoints.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/api-keys` | List API keys |
| `POST` | `/v1/api-keys` | Create API key |
| `DELETE` | `/v1/api-keys/{keyId}` | Deactivate API key |
| `GET` | `/v1/api-keys/{keyId}` | Get API key |
| `PATCH` | `/v1/api-keys/{keyId}` | Update API key |
| `PUT` | `/v1/api-keys/{keyId}/policy-bindings` | Replace or clear API-key policy bindings |
| `POST` | `/v1/api-keys/{keyId}/policy-profiles` | Create API-key policy profile |
| `POST` | `/v1/api-keys/{keyId}/policy-profiles/{profileId}/revisions` | Create API-key policy revision |
| `POST` | `/v1/api-keys/{keyId}/policy-profiles/{profileId}/revisions/{revisionId}/activate` | Activate API-key policy revision |
| `POST` | `/v1/api-keys/{keyId}/rotate` | Rotate API key |

---

### Wallets
Source: https://platform.solana.com/docs/reference/api/wallets

> Wallet signing provider configuration and wallet management.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `DELETE` | `/v1/wallets` | Delete wallet |
| `GET` | `/v1/wallets` | List wallets |
| `POST` | `/v1/wallets` | Create wallet |
| `GET` | `/v1/wallets/{walletId}` | Get wallet by ID |
| `PATCH` | `/v1/wallets/{walletId}` | Update wallet |
| `GET` | `/v1/wallets/aggregate` | Aggregate wallet balances |
| `GET` | `/v1/wallets/approval-requests` | List wallet approval requests |
| `GET` | `/v1/wallets/approval-requests/{approvalRequestId}` | Get wallet approval request |
| `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/approve` | Approve wallet approval request |
| `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/cancel` | Cancel wallet approval request |
| `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/reject` | Reject wallet approval request |
| `GET` | `/v1/wallets/config` | Get wallet signing config |
| `GET` | `/v1/wallets/configs` | List wallet signing configs |
| `POST` | `/v1/wallets/default-wallet` | Set default wallet |
| `POST` | `/v1/wallets/initialize` | Initialize wallet signing |
| `GET` | `/v1/wallets/public-key` | Get wallet public key |
| `POST` | `/v1/wallets/signer-check` | Check signer via memo transaction |
| `POST` | `/v1/wallets/switch` | Switch wallet signing provider |
| `GET` | `/v1/wallets/switch-options` | List switch provider options |

---

### Projects
Source: https://platform.solana.com/docs/reference/api/projects

> Project and project member management.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/projects` | List projects |
| `DELETE` | `/v1/projects/{projectId}` | Archive project |
| `GET` | `/v1/projects/{projectId}` | Get project |
| `PATCH` | `/v1/projects/{projectId}` | Update project |
| `GET` | `/v1/projects/{projectId}/api-keys` | List project API keys |
| `POST` | `/v1/projects/{projectId}/api-keys` | Create project API key |
| `GET` | `/v1/projects/{projectId}/members` | List project members |
| `POST` | `/v1/projects/{projectId}/members` | Add project member |
| `DELETE` | `/v1/projects/{projectId}/members/{memberId}` | Remove project member |
| `PATCH` | `/v1/projects/{projectId}/members/{memberId}` | Update project member |

---

### Issuance
Source: https://platform.solana.com/docs/reference/api/issuance

> Token issuance, allowlists, and lifecycle operations.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/issuance/templates` | List token templates |
| `GET` | `/v1/issuance/templates/{templateId}` | Get token template |
| `GET` | `/v1/issuance/tokens` | List tokens |
| `POST` | `/v1/issuance/tokens` | Create token |
| `GET` | `/v1/issuance/tokens/{tokenId}` | Get token |
| `PATCH` | `/v1/issuance/tokens/{tokenId}` | Update token |
| `GET` | `/v1/issuance/tokens/{tokenId}/allowlist` | List token allowlist |
| `POST` | `/v1/issuance/tokens/{tokenId}/allowlist` | Add token allowlist entry |
| `DELETE` | `/v1/issuance/tokens/{tokenId}/allowlist/{entryId}` | Remove token allowlist entry |
| `GET` | `/v1/issuance/tokens/{tokenId}/allowlist/labels` | List token allowlist labels |
| `GET` | `/v1/issuance/tokens/{tokenId}/audit` | Get asset audit history |
| `POST` | `/v1/issuance/tokens/{tokenId}/authority` | Execute authority update |
| `POST` | `/v1/issuance/tokens/{tokenId}/authority/prepare` | Prepare authority update |
| `POST` | `/v1/issuance/tokens/{tokenId}/burn` | Execute burn |
| `POST` | `/v1/issuance/tokens/{tokenId}/burn/prepare` | Prepare burn transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/deploy` | Deploy token |
| `POST` | `/v1/issuance/tokens/{tokenId}/deploy/confirm` | Confirm non-custodial deploy |
| `POST` | `/v1/issuance/tokens/{tokenId}/deploy/prepare` | Prepare token deploy transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/deploy/prepare-metadata` | Prepare metadata-URI follow-up transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/force-burn` | Execute force burn |
| `POST` | `/v1/issuance/tokens/{tokenId}/force-burn/prepare` | Prepare force burn transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/freeze` | Freeze account |
| `GET` | `/v1/issuance/tokens/{tokenId}/frozen` | List frozen accounts |
| `GET` | `/v1/issuance/tokens/{tokenId}/metadata.json` | Get public token metadata JSON |
| `POST` | `/v1/issuance/tokens/{tokenId}/mint` | Execute mint |
| `POST` | `/v1/issuance/tokens/{tokenId}/mint/prepare` | Prepare mint transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/pause` | Pause token transfers |
| `POST` | `/v1/issuance/tokens/{tokenId}/seize` | Execute seize |
| `POST` | `/v1/issuance/tokens/{tokenId}/seize/prepare` | Prepare seize transaction |
| `POST` | `/v1/issuance/tokens/{tokenId}/supply/refresh` | Refresh cached token supply |
| `GET` | `/v1/issuance/tokens/{tokenId}/transactions` | List token transactions |
| `POST` | `/v1/issuance/tokens/{tokenId}/unfreeze` | Unfreeze account |
| `POST` | `/v1/issuance/tokens/{tokenId}/unpause` | Unpause token transfers |
| `GET` | `/v1/issuance/tokens/facets` | List token filter facets |
| `GET` | `/v1/issuance/transactions` | List issuance transactions |

---

### Payments
Source: https://platform.solana.com/docs/reference/api/payments

> Wallet balances, transfer execution, policies, and ramps.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/payments/ramps/offramp/currency` | List off-ramp currency support |
| `GET` | `/v1/payments/ramps/onramp/currency` | List on-ramp currency support |
| `POST` | `/v1/payments/ramps/onramp/quote` | Create on-ramp quote |
| `POST` | `/v1/payments/ramps/sandbox/simulate` | Simulate sandbox transfer |
| `GET` | `/v1/payments/recurring-payments` | List recurring payments |
| `POST` | `/v1/payments/recurring-payments` | Create recurring payment |
| `GET` | `/v1/payments/recurring-payments/{id}` | Get recurring payment |
| `PATCH` | `/v1/payments/recurring-payments/{id}` | Update recurring payment |
| `POST` | `/v1/payments/recurring-payments/{id}/activate` | Activate recurring payment |
| `POST` | `/v1/payments/recurring-payments/{id}/cancel` | Cancel recurring payment |
| `POST` | `/v1/payments/recurring-payments/{id}/collect` | Collect recurring payment |
| `POST` | `/v1/payments/recurring-payments/{id}/resume` | Resume recurring payment |
| `GET` | `/v1/payments/subscription-plans` | List subscription plans |
| `POST` | `/v1/payments/subscription-plans` | Create subscription plan |
| `GET` | `/v1/payments/subscription-plans/{planId}` | Get subscription plan |
| `PATCH` | `/v1/payments/subscription-plans/{planId}` | Update subscription plan |
| `POST` | `/v1/payments/subscription-plans/{planId}/prepare-create` | Prepare subscription plan creation |
| `GET` | `/v1/payments/subscriptions` | List subscriptions |
| `POST` | `/v1/payments/subscriptions` | Create subscription |
| `GET` | `/v1/payments/subscriptions/{subscriptionId}` | Get subscription |
| `PATCH` | `/v1/payments/subscriptions/{subscriptionId}` | Update subscription |
| `GET` | `/v1/payments/subscriptions/{subscriptionId}/collection-attempts` | List subscription collection attempts |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/collection-attempts` | Create subscription collection attempt |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-authorization` | Prepare subscription authorization |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-cancel` | Prepare subscription cancellation |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-collection` | Prepare subscription collection |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-resume` | Prepare subscription resume |
| `GET` | `/v1/payments/transfer-batches` | List transfer batches |
| `POST` | `/v1/payments/transfer-batches` | Create transfer batch |
| `GET` | `/v1/payments/transfer-batches/{batchId}` | Get transfer batch |
| `POST` | `/v1/payments/transfer-batches/estimate` | Estimate transfer batch |
| `GET` | `/v1/payments/transfers` | List transfers |
| `POST` | `/v1/payments/transfers` | Execute transfer (custody) |
| `GET` | `/v1/payments/transfers/{transferId}` | Get transfer |
| `GET` | `/v1/payments/wallets/{walletId}/balances` | Get wallet balances |
| `GET` | `/v1/payments/wallets/{walletId}/policies` | Get wallet policy |
| `PUT` | `/v1/payments/wallets/{walletId}/policies` | Update wallet policy |
| `GET` | `/v1/payments/wallets/{walletId}/policies/evaluations` | List wallet policy evaluations |
| `GET` | `/v1/payments/wallets/{walletId}/policies/evaluations/{policyEvaluationId}` | Get wallet policy evaluation |
| `GET` | `/v1/payments/wallets/{walletId}/policies/revisions` | List wallet policy revisions |

---

### Policies
Source: https://platform.solana.com/docs/reference/api/policies

> Wallet and API-key policy-control inventory.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/policies` | List policy controls |

---

### Compliance
Source: https://platform.solana.com/docs/reference/api/compliance

> Risk and compliance screening endpoints.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/compliance/address-screenings` | Screen an address across configured compliance providers |

---

### Asset Profiles
Source: https://platform.solana.com/docs/reference/api/asset-profiles

> Issued-asset identity and metadata profiles, plus the public token metadata URI.

| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/issuance/asset-profiles` | List asset profiles |
| `POST` | `/v1/issuance/asset-profiles` | Create token with asset profile |
| `DELETE` | `/v1/issuance/asset-profiles/{profileId}` | Archive asset profile |
| `GET` | `/v1/issuance/asset-profiles/{profileId}` | Get asset profile |
| `PATCH` | `/v1/issuance/asset-profiles/{profileId}` | Update asset profile |
| `GET` | `/v1/issuance/asset-profiles/by-token/{tokenId}` | Get asset profile by token |
| `GET` | `/v1/issuance/asset-profiles/field-options` | Get asset profile field options |

## Notes
- Generated from docs source. Hidden or internal-only APIs are intentionally excluded.
