Wallet Swap
Swap between BRS, BRLA and USDC from a user's own wallet. Quoted 1:1 on the BRL pair, settled on-chain.
POST /api/wallet/swap
Swaps one asset in the API key user's wallet for another, across chains, and delivers the result to that same user's wallet. Gas is paid by Hodle.
| From | To | Route |
|---|---|---|
BRS | BRLA | direct |
BRLA | BRS | direct |
BRLA | USDC | direct |
USDC | BRLA | direct |
BRS | USDC | two hops, through BRLA |
USDC | BRS | two hops, through BRLA |
Each asset lives on one chain: BRS on Solana, BRLA on Polygon, USDC on Base. You do not pass a network — the pair determines it.
BRS↔USDCis not atomic. There is no direct market between them, so the swap runs as two hops withBRLAheld in between. If the second hop does not complete, the wallet holdsBRLA— not the asset you asked for and not the one you paid in. That state is reported asSTRANDED, and it is distinct fromFAILED, which means nothing moved at all. Always read the status before treating a two-hop swap as done.
Currently only
BRS-origin swaps execute.BRS → BRLAcompletes;BRS → USDCcompletes its first hop and reportsSTRANDED. Every pair that starts fromBRLAorUSDCis refused up front with501 EVM_ORIGIN_NOT_SUPPORTED— nothing is debited. This is a temporary limitation, and the refusal happens before any money moves.
Self-custodial signing. These wallets are non-custodial — Hodle stores only ciphertext and can never sign on its own. Every swap must carry the wallet's
walletPinand itsprotectedSymmetricKey; the server uses them to unlock the key transiently in memory to sign, then discards them. You (the platform) are the custodian of your subaccounts' PINs and must pass them per request.
Request
curl --request POST \
--url https://api.hodle.com.br/api/wallet/swap \
--header "Authorization: Bearer $API_KEY" \
--header "Content-Type: application/json" \
--data '{
"fromAsset": "BRS",
"toAsset": "BRLA",
"amount": "10.00",
"walletPin": "424242",
"protectedSymmetricKey": "AoofiKHyVRLvdrknnXzoIh1Gd1YTwLaOBn4ibm103a4d...",
"reference": "treasury-rebalance-#77"
}'const res = 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: 'BRS',
toAsset: 'BRLA',
amount: '10.00',
walletPin: '424242',
protectedSymmetricKey: process.env.HODLE_PROTECTED_SYMMETRIC_KEY,
reference: 'treasury-rebalance-#77',
}),
})
const swap = await res.json()import os, requests
res = requests.post(
"https://api.hodle.com.br/api/wallet/swap",
headers={"Authorization": f"Bearer {os.environ['HODLE_API_KEY']}"},
json={
"fromAsset": "BRS",
"toAsset": "BRLA",
"amount": "10.00",
"walletPin": "424242",
"protectedSymmetricKey": os.environ["HODLE_PROTECTED_SYMMETRIC_KEY"],
"reference": "treasury-rebalance-#77",
},
)
swap = res.json()Parameters
| Field | Type | Required | Description |
|---|---|---|---|
fromAsset | string | yes | BRS, BRLA or USDC. |
toAsset | string | yes | BRS, BRLA or USDC. Must differ from fromAsset. |
amount | string | yes | Decimal string in fromAsset units, e.g. "10.00". Maximum 10000. |
walletPin | string | yes | PIN of the wallet being spent. |
protectedSymmetricKey | string | yes | The wallet's protected symmetric key, from /api/wallet/keys. |
subAccountId | string | no | Swap inside a subaccount's wallet instead of your main account's. |
reference | string | no | Your own identifier, echoed back on the swap. |
Response
The call returns as soon as the first hop is broadcast on-chain — not when the destination asset has been delivered. Poll the status endpoint for settlement.
{
"success": true,
"swapId": "b0c1123c-8af5-417b-aff4-9e9e55b5968d",
"status": "RUNNING",
"legs": [
{
"index": 0,
"fromAsset": "BRS",
"toAsset": "BRLA",
"status": "BROADCAST",
"quoteId": "9a43659f-c3f3-45fc-af5d-bf2c442d3303",
"originTxHash": "2mkeEjXyFUrrvuCLeXJrpVLJjWvacMdCmQFds2N4V5AePXh2fkGviTFQvC7cYczEaNX9WAoDxZBwdTXnipyoaqJY",
"expectedAmountOut": "10000000000000000000"
}
]
}Swap status
| Status | Meaning |
|---|---|
PENDING | Recorded, nothing broadcast yet. |
RUNNING | At least one hop is broadcast and settling. |
COMPLETED | Every hop delivered. |
STRANDED | A hop delivered and the next did not. The wallet holds the intermediate asset. |
FAILED | Nothing moved. |
Leg status
PENDING → BROADCAST → FULFILLED, or FAILED. amountIn and expectedAmountOut are raw integer strings in each asset's own decimals: BRS and USDC have 6, BRLA has 18.
GET /api/wallet/swap/:swapId
Reads a swap and refreshes every broadcast hop against the settlement network, so destinationTxHash appears here as soon as the asset lands.
curl --request GET \
--url https://api.hodle.com.br/api/wallet/swap/b0c1123c-8af5-417b-aff4-9e9e55b5968d \
--header "Authorization: Bearer $API_KEY"{
"success": true,
"swapId": "b0c1123c-8af5-417b-aff4-9e9e55b5968d",
"status": "RUNNING",
"fromAsset": "BRS",
"toAsset": "BRLA",
"amountIn": "10000000",
"error": null,
"legs": [
{
"index": 0,
"fromAsset": "BRS",
"toAsset": "BRLA",
"status": "BROADCAST",
"originTxHash": "2mkeEjXyFUrrvuCLeXJrpVLJjWvacMdCmQFds2N4V5AePXh2fkGviTFQvC7cYczEaNX9WAoDxZBwdTXnipyoaqJY",
"expectedAmountOut": "10000000000000000000",
"destinationTxHash": "0x557cb723b4bc364e9546c899b41f2ae5517015985aa7b750db2e6f7b186563aa",
"destinationTxUrl": "https://polygonscan.com/tx/0x557cb723...",
"refundTxHash": null
}
]
}A swap is only readable by the account that created it.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | UNSUPPORTED_PAIR | That fromAsset → toAsset combination has no route. |
400 | — | Invalid amount, amount above 10000, or a missing required field. |
400 | — | Wrong walletPin or protectedSymmetricKey. |
404 | — | No wallet found for the account or subaccount. |
429 | — | Too many failed PIN attempts. Back off and retry. |
501 | EVM_ORIGIN_NOT_SUPPORTED | The pair starts from BRLA or USDC. Nothing was debited. |
502 | — | The swap could not be completed. Read status on the body to tell FAILED (nothing moved) from STRANDED (value stopped mid-route). |
When to use this vs. wallet/transfer
wallet/transfer moves the same asset to another address or subaccount. wallet/swap changes which asset the wallet holds, keeping it with the same owner. To do both, swap first and then transfer the result.
BRS also carries its own gating — see BRS.