DePix swap
Swap your existing DePix on Spark into BTC in a Spark wallet or a payment to a Lightning address, using Flashnet.
Use POST /api/wallet/swap to spend DePix already held in your Hodle Spark wallet. Hodle exchanges it on the DePix/BTC Flashnet pool and delivers BTC to a Spark address or pays a Lightning address. This operation does not create a PIX charge and cannot spend DePix held on Liquid.
Prerequisites
- A production API key and an account with swaps enabled (
SWAP). Spark DePix swaps are unavailable in sandbox. - An initialized Spark wallet with spendable DePix. Unlock the wallet once in the app to register its Spark identity.
- The wallet PIN and its
protectedSymmetricKey, available through Wallet Keys. The PIN must unlock the wallet selected bysubAccountId, when supplied.
A supplied destination changes where BTC is delivered. It does not select the wallet to debit or authorize spending from an arbitrary Spark address.
POST /api/wallet/swap
The API verifies wallet authorization, records the request, and returns 202 Accepted. The worker checks the pool output before transferring DePix. Follow the swapId to learn whether delivery completed.
const response = await fetch('https://api.hodle.com.br/api/wallet/swap', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HODLE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fromAsset: 'DEPIX',
toAsset: 'BTC',
network: 'spark',
amount: '25.00',
clientRequestId: 'depix-order-123',
minAmountOutSats: 400,
maxSlippageBps: 100,
walletPin: process.env.HODLE_WALLET_PIN,
protectedSymmetricKey: process.env.HODLE_PROTECTED_SYMMETRIC_KEY,
}),
})
const swap = await response.json()Omit address to receive BTC in the selected wallet's Spark address, or pass another valid mainnet spark1… address.
{
"success": true,
"swapId": "f3cda087-85f9-4e17-a37a-208e58231fe1",
"clientRequestId": "depix-order-123",
"status": "PENDING",
"fromAsset": "DEPIX",
"toAsset": "BTC",
"amountIn": "2500000000",
"network": "spark",
"destination": "spark1pgss92n427yh3ghz5dq0j93ph3p9lrmhy7enrus02uusmd6ktlx23t88en8f8j",
"minAmountOutSats": 400,
"maxSlippageBps": 100,
"maxLightningFeeSats": 0
}amount is a decimal string in whole DePix. amountIn is an integer string in raw token units: 1 DePix = 100,000,000 units. BTC amounts in the response and tracking legs are satoshis.
Request fields
| Field | Required | Description |
|---|---|---|
fromAsset | Yes | DEPIX. The source network is Spark. |
toAsset | Yes | BTC. |
network | Recommended | Delivery network: spark or lightning. Omission retains the Lightning default. Always send it explicitly in new integrations. |
amount | Yes | Positive decimal string with up to 8 decimal places; at most 10,000 DePix. Scientific notation is rejected. |
clientRequestId | Yes | Unique per owner; 1–100 letters, digits, _, or -. Reuse the same ID when retrying the same request. |
walletPin | Yes | PIN for the selected wallet. Never log it. Three invalid attempts lock the wallet for 24 hours. |
protectedSymmetricKey | Yes | Encrypted key envelope from Wallet Keys. Never log it. |
subAccountId | No | A sub-account owned by the API-key holder. Its wallet is debited; the owner's default wallet is not a fallback. |
address | No | A mainnet Spark address for spark, or user@domain for lightning. Defaults to the selected wallet's address for that network. |
minAmountOutSats | No | Minimum BTC delivered to the requested destination. Integer, at least 400; defaults to 400. For Lightning, applies after reserving the fee allowance. |
maxSlippageBps | No | Integer from 0 to 1,000; defaults to 100 (1%). Passed to Flashnet along with the minimum output. |
maxLightningFeeSats | No | Integer from 0 to 10,000; defaults to 10 for Lightning, ignored for Spark. Maximum routing allowance, not a fee charged in full. |
reference | No | Your label for reconciliation. It is not an idempotency key. |
Lightning delivery
Use the same endpoint and credentials with these fields:
{
"fromAsset": "DEPIX",
"toAsset": "BTC",
"network": "lightning",
"amount": "25.00",
"clientRequestId": "depix-lightning-124",
"address": "[email protected]",
"minAmountOutSats": 400,
"maxSlippageBps": 100,
"maxLightningFeeSats": 10,
"walletPin": "<wallet PIN>",
"protectedSymmetricKey": "<encrypted key envelope>"
}Hodle first transfers the entire BTC swap output to your Spark wallet. That wallet then pays an invoice obtained from the Lightning address for swap output minus maxLightningFeeSats. Only the actual routing fee is spent; the unused allowance stays in your Spark wallet.
For example, a 2,000-sat swap with a 10-sat allowance pays a 1,990-sat invoice. If the actual routing fee is 3 sats, 7 sats remain in your Spark wallet. A route that needs more than the allowance is not authorized. A Lightning failure does not repeat the DePix swap.
BOLT11 invoices are not accepted as the address input because the final payable amount is known only after the swap. The Lightning service must accept the resulting amount; an invalid or unavailable service can leave the BTC in your Spark wallet without completing the payment.
Track settlement
curl --request GET \
--url 'https://api.hodle.com.br/api/wallet/swap/f3cda087-85f9-4e17-a37a-208e58231fe1' \
--header "Authorization: Bearer $HODLE_API_KEY"For a sub-account swap, append ?subAccountId=YOUR_SUBACCOUNT_ID. An unrelated API key, or a request for the wrong sub-account, receives 404.
Poll every few seconds until a terminal state. A successful POST only confirms acceptance; COMPLETED confirms destination delivery. This endpoint does not emit DEPOSIT_ASSET_SUCCESS; that event belongs to PIX deposits.
| Status | Meaning and action |
|---|---|
PENDING | Recorded and awaiting the worker. |
RUNNING | A worker has claimed the request. Do not create a second swap while it runs. |
COMPLETED | Spark transfer, or the final Lightning payment, was confirmed. |
FAILED | The operation stopped before any funds were known to move, such as expired authorization or insufficient output. Inspect error. |
STRANDED | Funds moved, or a transfer outcome is uncertain. Inspect the legs and reconcile the existing operation before attempting anything again. For a Lightning failure after BTC delivery, BTC is already in your wallet and may be temporarily locked by the payment. |
The response includes legs, each with status, originChain, destinationChain, originTxHash, destinationTxHash, actualAmountOut, and error:
| Leg | Movement | Evidence |
|---|---|---|
| 0 | Your Spark DePix → Hodle settlement wallet | originTxHash: token transfer ID. |
| 1 | DePix → BTC on Flashnet | swapRequestId: Flashnet request ID; destinationTxHash: pool BTC transfer ID; actualAmountOut: gross sats. |
| 2 | Settlement wallet → Spark destination | destinationTxHash: Spark BTC transfer ID. For Lightning, the destination is your own Spark wallet. |
| 3, Lightning only | Your Spark wallet → Lightning recipient | destinationTxHash: Lightning request ID; actualAmountOut: invoice sats; feeSats: actual routing fee when available. |
Confirmed legs remain recorded even when a later step fails. Repeated worker delivery cannot repeat the debit. An interrupted worker can leave RUNNING until reconciliation; a Lightning payment still unconfirmed after the bounded settlement check is reported as STRANDED, never as completed.
Retrying HTTP requests
Retry the identical request with the same clientRequestId after an HTTP timeout. Once the wallet unlock check is free, you receive 200 with the original swapId and current status, and no new job is created. Changing the amount, destination, network, wallet, limits, or reference while reusing the ID returns 409 IDEMPOTENCY_CONFLICT.
A replay of a failed request reports its existing failure; it does not restart settlement. Never use a new ID to recover an uncertain or stranded transfer. Wallet authorization expires after five minutes if the worker has not consumed it.
Costs and errors
The pool fee is included in the BTC output; the current DePix/BTC pool lists a 1.40% fee. There is no PIX deposit fee on this balance swap and no additional Hodle swap markup in this route. Lightning routing fees are paid from the BTC already credited to the user's Spark wallet. See the pool for the token and pool identifiers.
| HTTP / code | Meaning |
|---|---|
400 INVALID_INPUT | Missing request ID, invalid amount, or excessive precision. Invalid limits also return 400. |
400 NO_SPARK_WALLET | The selected wallet has no initialized Spark identity or encrypted mnemonic. |
400 INVALID_SPARK_ADDRESS | Invalid or non-mainnet Spark destination. |
400 INVALID_LIGHTNING_DESTINATION | Missing or malformed Lightning address. |
400 BOLT11_NOT_SUPPORTED | Use a Lightning address instead of a fixed invoice. |
401 INVALID_PIN | The PIN/key envelope does not unlock the selected wallet. |
403 NOT_ENABLED | Swaps are not enabled for the account. |
501 | Sandbox API keys cannot submit swaps; no live Spark operation runs. |
404 | User, owned sub-account, or scoped swap was not found. |
409 IDEMPOTENCY_CONFLICT | Request ID already used with different parameters. |
429 RATE_LIMITED | Wallet PIN lockout, or another unlock is in progress. Retry a busy unlock later with the same request ID. |
503 QUEUE_UNAVAILABLE | Could not enqueue; the response includes the swap ID. |
Pool minimums, spendable balance, and live settlement are checked by the worker. Their failures appear in the tracking response rather than as a synchronous POST rejection.