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.

FromToRoute
BRSBRLAdirect
BRLABRSdirect
BRLAUSDCdirect
USDCBRLAdirect
BRSUSDCtwo hops, through BRLA
USDCBRStwo 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.

BRSUSDC is not atomic. There is no direct market between them, so the swap runs as two hops with BRLA held in between. If the second hop does not complete, the wallet holds BRLA — not the asset you asked for and not the one you paid in. That state is reported as STRANDED, and it is distinct from FAILED, 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 → BRLA completes; BRS → USDC completes its first hop and reports STRANDED. Every pair that starts from BRLA or USDC is refused up front with 501 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 walletPin and its protectedSymmetricKey; 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

FieldTypeRequiredDescription
fromAssetstringyesBRS, BRLA or USDC.
toAssetstringyesBRS, BRLA or USDC. Must differ from fromAsset.
amountstringyesDecimal string in fromAsset units, e.g. "10.00". Maximum 10000.
walletPinstringyesPIN of the wallet being spent.
protectedSymmetricKeystringyesThe wallet's protected symmetric key, from /api/wallet/keys.
subAccountIdstringnoSwap inside a subaccount's wallet instead of your main account's.
referencestringnoYour 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

StatusMeaning
PENDINGRecorded, nothing broadcast yet.
RUNNINGAt least one hop is broadcast and settling.
COMPLETEDEvery hop delivered.
STRANDEDA hop delivered and the next did not. The wallet holds the intermediate asset.
FAILEDNothing moved.

Leg status

PENDINGBROADCASTFULFILLED, 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

StatusCodeMeaning
400UNSUPPORTED_PAIRThat fromAssettoAsset combination has no route.
400Invalid amount, amount above 10000, or a missing required field.
400Wrong walletPin or protectedSymmetricKey.
404No wallet found for the account or subaccount.
429Too many failed PIN attempts. Back off and retry.
501EVM_ORIGIN_NOT_SUPPORTEDThe pair starts from BRLA or USDC. Nothing was debited.
502The 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.