> ## Documentation Index
> Fetch the complete documentation index at: https://agents-docs.lightspark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-Custody Wallet Integration

> Send and receive cryptocurrency to and from user-controlled wallets

## Overview

Grid supports sending Bitcoin via Lightning Network to self-custody wallets using Spark wallet addresses. This enables users to maintain full control of their crypto while benefiting from Grid's fiat-to-crypto conversion and payment rails.

> [!NOTE]
>
> Spark wallets use the Lightning Network for instant, low-cost Bitcoin
> transactions. Users can receive payments directly to their self-custody
> wallets without Grid holding their funds.

## How it works

1. **User provides wallet address** - Get Spark wallet address from user
2. **Create quote** - Generate quote for fiat-to-crypto or crypto-to-crypto transfer
3. **Execute transfer** - Send crypto directly to user's wallet
4. **Instant settlement** - Lightning Network provides near-instant confirmation

## Prerequisites

* Customer created in Grid
* Valid Spark wallet address from user
* Webhook endpoint for payment notifications (optional, for status updates)

## Sending crypto to self-custody wallets

### Step 1: Collect wallet address

Request the user's Spark wallet address. Spark addresses start with `spark1`:

```javascript theme={null}
function validateSparkAddress(address) {
  // Spark addresses start with spark1 and are typically 90+ characters
  if (!address.startsWith("spark1")) {
    throw new Error("Invalid Spark wallet address");
  }

  if (address.length < 90) {
    throw new Error("Spark address appears incomplete");
  }

  return address;
}

// Example valid address
const sparkAddress =
  "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu";
```

> [!WARNING]
>
> Always validate wallet addresses before creating quotes. Invalid addresses
> will cause transaction failures and potential fund loss.

### Step 2: Create external account for the wallet

Register the Spark wallet as an external account:

```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
    "currency": "BTC",
    "accountInfo": {
      "accountType": "SPARK_WALLET",
      "address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
    }
  }'
```

```javascript Node.js theme={null}
const externalAccount = await fetch(
  "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts",
  {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
      currency: "BTC",
      accountInfo: {
        accountType: "SPARK_WALLET",
        address: sparkAddress,
      },
    }),
  }
).then((r) => r.json());

console.log("External account ID:", externalAccount.id);
```

```python Python theme={null}
import requests
import base64

credentials = base64.b64encode(
    f"{api_token_id}:{api_secret}".encode()
).decode()

response = requests.post(
    'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts',
    headers={
        'Authorization': f'Basic {credentials}',
        'Content-Type': 'application/json'
    },
    json={
        'customerId': 'Customer:019542f5-b3e7-1d02-0000-000000000001',
        'currency': 'BTC',
        'accountInfo': {
            'accountType': 'SPARK_WALLET',
            'address': spark_address
        }
    }
)

external_account = response.json()
print(f"External account ID: {external_account['id']}")
```

> [!TIP]
>
> Store the external account ID for future transfers. Users can reuse the same
> wallet address for multiple transactions.

### Step 3: Create and execute a quote

#### Option A: From fiat to self-custody wallet

Convert fiat directly to crypto in user's wallet:

```javascript theme={null}
// Create quote for fiat-to-crypto
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
  method: "POST",
  body: JSON.stringify({
    source: {
      sourceType: "REALTIME_FUNDING",
      customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
      currency: "USD",
    },
    destination: {
      destinationType: "ACCOUNT",
      accountId: externalAccount.id,
    },
    lockedCurrencySide: "SENDING",
    lockedCurrencyAmount: 10000, // $100.00
    description: "Buy Bitcoin to self-custody wallet",
  }),
}).then((r) => r.json());

// Display payment instructions to user
console.log("Send fiat to:", quote.paymentInstructions);
console.log("Will receive:", `${quote.totalReceivingAmount / 100000000} BTC`);
```

#### Option B: From internal account to self-custody wallet

Transfer crypto from internal account to user's wallet:

```javascript theme={null}
// Same-currency transfer: immediatelyExecute creates and executes in one request
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
  method: "POST",
  body: JSON.stringify({
    source: {
      sourceType: "ACCOUNT",
      accountId: "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
    },
    destination: {
      destinationType: "ACCOUNT",
      accountId: externalAccount.id,
    },
    lockedCurrencySide: "SENDING",
    lockedCurrencyAmount: 100000, // 0.001 BTC in satoshis
    immediatelyExecute: true,
  }),
}).then((r) => r.json());

console.log("Transfer initiated:", quote.transactionId);
```

### Step 4: Monitor transfer completion

Track the transfer status via `OUTGOING_PAYMENT.<STATUS>` webhooks:

```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
  const { type, data } = req.body;

  switch (type) {
    case "OUTGOING_PAYMENT.COMPLETED":
      await notifyUser(data.customerId, {
        message: "Bitcoin sent to your wallet!",
        amount: `${data.receivedAmount.amount / 100000000} BTC`,
      });
      break;

    case "OUTGOING_PAYMENT.FAILED":
      await notifyUser(data.customerId, {
        message: "Transfer failed",
        reason: data.failureReason,
        action: "Please verify your wallet address",
      });
      // Refund webhook (OUTGOING_PAYMENT.REFUND_*) will follow
      break;

    case "OUTGOING_PAYMENT.REFUND_COMPLETED":
      await notifyUser(data.customerId, {
        message: "Refund completed. Funds returned to your account.",
      });
      break;
  }

  res.status(200).json({ received: true });
});
```

See the [Transaction Lifecycle](https://agents-docs.lightspark.com/platform-overview/core-concepts/transaction-lifecycle.md) guide for all status transitions and refund handling.

## Best practices

> [accordion] Validate wallet addresses
>
> Always validate Spark addresses before processing:
>
> ```javascript theme={null}
> function validateSparkAddress(address) {
>   // Check format
>   if (!address.startsWith("spark1")) {
>     return { valid: false, error: "Must start with spark1" };
>   }
>
>   // Check length (typical Spark addresses are 90+ chars)
>   if (address.length < 90) {
>     return { valid: false, error: "Address too short" };
>   }
>
>   // Check for common typos
>   if (address.includes(" ") || address.includes("\n")) {
>     return { valid: false, error: "Address contains whitespace" };
>   }
>
>   return { valid: true };
> }
> ```

> [accordion] Handle Lightning Network specifics
>
> Lightning Network has unique characteristics:
>
> ```javascript theme={null}
> const lightningLimits = {
>   minAmount: 1000, // 0.00001 BTC (1000 satoshis)
>   maxAmount: 10000000, // 0.1 BTC (10M satoshis)
>   settlementTime: "Instant (typically < 10 seconds)",
>   fees: "Very low (typically < 1%)",
> };
>
> function validateLightningAmount(satoshis) {
>   if (satoshis < lightningLimits.minAmount) {
>     throw new Error(`Minimum amount is ${lightningLimits.minAmount} sats`);
>   }
>
>   if (satoshis > lightningLimits.maxAmount) {
>     throw new Error(`Maximum amount is ${lightningLimits.maxAmount} sats`);
>   }
>
>   return true;
> }
> ```

> [accordion] Provide clear user instructions
>
> Help users understand the process:
>
> ```javascript theme={null}
> function getWalletInstructions(walletType) {
>   return {
>     SPARK_WALLET: {
>       title: "Lightning Network Wallet",
>       steps: [
>         "Open your Lightning wallet app",
>         'Select "Send" or "Pay"',
>         "Scan the QR code or paste the address",
>         "Confirm the amount and send",
>         "Funds arrive instantly",
>       ],
>       compatibleWallets: [
>         "Spark Wallet",
>         "Phoenix",
>         "Breez",
>         "Muun",
>         "Blue Wallet (Lightning)",
>       ],
>     },
>   };
> }
> ```

> [accordion] Handle failed transfers
>
> Implement retry logic for common failures:
>
> ```javascript theme={null}
> async function handleTransferFailure(transaction) {
>   const { failureReason } = transaction;
>
>   const retryableReasons = [
>     "TEMPORARY_NETWORK_ERROR",
>     "INSUFFICIENT_LIQUIDITY",
>     "ROUTE_NOT_FOUND",
>   ];
>
>   if (retryableReasons.includes(failureReason)) {
>     // Retry after delay
>     await delay(5000);
>     return await retryTransfer(transaction.quoteId);
>   }
>
>   // Non-retryable errors
>   const errorMessages = {
>     INVALID_ADDRESS:
>       "The wallet address is invalid. Please verify and try again.",
>     AMOUNT_TOO_SMALL:
>       "Amount is below minimum. Lightning Network requires at least 1000 satoshis.",
>     AMOUNT_TOO_LARGE:
>       "Amount exceeds Lightning Network limits. Consider splitting into multiple transfers.",
>   };
>
>   return {
>     canRetry: false,
>     message:
>       errorMessages[failureReason] ||
>       "Transfer failed. Please contact support.",
>   };
> }
> ```

## Testing in sandbox

Test self-custody wallet flows in sandbox mode:

```bash theme={null}
# Step 1: Create the external account for the Spark wallet
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:...",
    "currency": "BTC",
    "accountInfo": {
      "accountType": "SPARK_WALLET",
      "address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
    }
  }'

# Step 2: Create and execute the quote
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "sourceType": "ACCOUNT",
      "accountId": "InternalAccount:..."
    },
    "destination": {
      "destinationType": "ACCOUNT",
      "accountId": "ExternalAccount:..."
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000,
    "immediatelyExecute": true
  }'
```

> [!TIP]
>
> In sandbox mode, transfers to Spark wallets complete instantly without
> requiring actual Lightning Network transactions.

## Next steps

![](https://agents-docs.lightspark.com/images/icons/arrow-left-right.svg)

Fiat-crypto conversion

Learn about on-ramp and off-ramp flows

[Open](https://agents-docs.lightspark.com/ramps/conversion-flows/fiat-crypto-conversion.md)

![](https://agents-docs.lightspark.com/images/icons/bank.svg)

External accounts

Manage external accounts including Spark wallets

[Open](https://agents-docs.lightspark.com/ramps/accounts/external-accounts.md)

![](https://agents-docs.lightspark.com/images/icons/bell.svg)

Webhooks

Set up webhooks to monitor transaction status

[Open](https://agents-docs.lightspark.com/ramps/platform-tools/webhooks.md)

![](https://agents-docs.lightspark.com/images/icons/sandbox.svg)

Sandbox testing

Test wallet integrations in sandbox mode

[Open](https://agents-docs.lightspark.com/ramps/platform-tools/sandbox-testing.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.