Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.hubra.app/llms.txt

Use this file to discover all available pages before exploring further.

Solana’s stake program enforces a 2 to 3 day deactivation period. Hubra’s instant unstake routes around it through Sanctum’s shared LST liquidity layer, so you can exit a position in a single block instead of waiting an epoch. This page covers what’s actually happening on-chain, which Sanctum endpoint each path uses, and how to wire the calls programmatically.
Hubra charges no protocol fee on unstake. Cost is price impact only, dynamic and quoted live by Sanctum, starting from around 0.05% in healthy markets.

What it covers

SourceOutputPathSanctum endpoint
Active native stake accountSOLOne transactionswap/depositStake
raSOLSOLOne transactionswap/token (instant)
raSOLSOL via stake accountTwo phases (~2 to 3d)swap/withdrawStake (slow)
Native stake account (slice)SOLSplit + routeStakeProgram.split then swap/depositStake
raUSDCUSDCVault direct-withdrawVoltr direct-withdraw (no Sanctum)
The first three are the Sanctum-routed flows. The last one is included because USDC withdraws are also “instant”; they just don’t pass through Sanctum.

How it works under the hood

Native instant: depositStake

Hubra calls Sanctum’s order endpoint:
POST https://swap.sanctum.so/v1/swap/depositStake/order
{
  "wallet":       "<your-wallet>",
  "stakeAccount": "<active-stake-account>",
  "outToken":     "So11111111111111111111111111111111111111112"
}
Sanctum returns an order response with:
  • tx — the unsigned transaction (server-normalized to standard base64 by Hubra).
  • outAmt — the SOL output in lamports.
  • swapSrcData.data.priceImpactPct — the price impact as a fraction.
The transaction does the following on-chain:
  1. Re-authorizes the stake account so Sanctum’s pool can claim it (StakeProgram.authorize).
  2. Calls Sanctum’s depositStake instruction. The pool absorbs the active stake and mints SOL to the wallet at the pool’s current rate.
  3. The pool runs deactivation in the background; you have already exited.
depositStake consumes the entire stake account. It does not take an amount; partial exits require splitting first (see below).

Liquid instant: pooled token swap

Same shape, different Sanctum endpoint:
POST https://swap.sanctum.so/v1/swap/token/order
{
  "wallet":   "<your-wallet>",
  "inToken":  "HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX",  // raSOL
  "outToken": "So11111111111111111111111111111111111111112",   // SOL
  "amount":   "<lamports>"
}
The transaction is a routed swap across Sanctum’s pooled LST liquidity. Because raSOL is a Sanctum preferred-partner LST, the pool depth is among the deepest on Solana — price impact at typical sizes is materially lower than depositStake at the same SOL notional.

Liquid slow: withdrawStake

For when you want zero price impact and can wait an epoch:
POST https://swap.sanctum.so/v1/swap/withdrawStake/order
{
  "wallet":      "<your-wallet>",
  "inToken":    "HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX",
  "voteAccount": "7K8DVxtNJGnMtUY1CQJT5jcs8sFGSZTDiG7kowvFpECh",
  "amount":      "<lamports>"
}
The transaction:
  1. Burns your raSOL.
  2. Creates a fresh native stake account, delegated to Hubra’s validator, funded with the underlying SOL.
  3. Returns control of the stake account to your wallet.
You then run StakeProgram.deactivate manually (or via POST /api/v1/unstake with kind: "deactivate"), wait for the deactivation epoch, and call POST /api/v1/withdraw to close the account. No fee, no price impact. Costs the same epoch wait as native staking.

Partial unstake on native: how the split works

depositStake is all-or-nothing. To exit only part of a native stake account, the source account is split first.
existing stake account (10 SOL active)

        │  StakeProgram.split

┌───────────────────┬───────────────────┐
│  slice (3 SOL)    │  remainder (7 SOL)│
│   ↓ depositStake  │     keeps earning │
│   SOL out         │                   │
└───────────────────┴───────────────────┘
On-chain, the split is a single instruction (StakeProgram.split) that creates a new stake account with the same delegation as the parent and the lamports you specified. The split account is active from the moment it exists; no re-activation epoch is needed. The split account is what gets routed through depositStake. The remaining stake account keeps earning rewards as if nothing happened.

Costs

Rent for the new (split) account~0.002 SOL
Sanctum price impact on the slicevaries
Hubra protocol feeNone
Network feesCovered by Hubra
In the app, the split + route happens inside a single transaction-card flow; you see one quote and one signature.

The order response and sanctum_order

Every Sanctum-routed build response carries the original Sanctum order alongside the unsigned transaction:
{
  "transaction":   "<base64 unsigned tx>",
  "hubra_token":   "<HMAC>",
  "route":         "sanctum",
  "sanctumKind":   "depositStake",
  "sanctum_order": {
    "inp":         "...",
    "out":         "...",
    "mode":        "ExactIn",
    "inpAmt":      "...",
    "outAmt":      "...",
    "swapSrcData": { "...": "..." },
    "tx":          "..."
  },
  "signers": ["<your-wallet>"]
}
When broadcasting, the agent must forward sanctum_order (along with sanctumKind) back to POST /api/v1/broadcast when using route: "sanctum". Sanctum’s execute endpoint independently validates the signed transaction’s message bytes against the original order and rejects mismatches — that is how the Sanctum router stays safe against tampering. If you broadcast via route: "rpc" (plain RPC), the chain itself does not need the order; the transaction is self-contained. You lose Sanctum’s MEV-protected broadcaster, but the on-chain effect is identical.

Sanctum kinds

The sanctumKind field tells /broadcast which Sanctum execute endpoint to use:
sanctumKindUsed bySanctum execute endpoint
tokenraSOL → SOL instant unstake; SOL → raSOL stakeswap/token/execute
depositStakeNative instant unstakeswap/depositStake/execute
withdrawStakeraSOL slow unstakeswap/withdrawStake/execute
depositSol(Reserved; not currently used by Hubra flows)swap/depositSol/execute
Forward the sanctumKind from the build response to /broadcast verbatim. Mismatching it will return 400 invalid_request.

Quoting before you sign

Always quote first for instant unstake at meaningful size. Pool depth changes minute to minute.
curl -X POST https://hubra.app/api/v1/quote \
  -H 'Content-Type: application/json' \
  -d '{
    "strategy": "sol-liquid-stake",
    "wallet":   "<your-wallet>",
    "amount":   "100"
  }'
Response:
{
  "strategy":       "sol-liquid-stake",
  "inAsset":        "raSOL",
  "outAsset":       "SOL",
  "inAmount":       "100",
  "outAmount":      "117.42",
  "priceImpactPct": 0.0028
}
For native instant, pass stakeAccount and the active stake amount in SOL:
curl -X POST https://hubra.app/api/v1/quote \
  -H 'Content-Type: application/json' \
  -d '{
    "strategy":     "sol-native-stake",
    "wallet":       "<your-wallet>",
    "stakeAccount": "<active-stake-account>",
    "amount":       "1.0"
  }'
Quotes are non-binding. Pool state drifts between quote and broadcast; the actual outAmt may differ slightly. Treat the quote as a live estimate, not a contract. The quote endpoint reuses Sanctum’s order endpoint server-side — the unsigned transaction the order also returns is discarded at the boundary. This is why the quote shape mirrors the build shape. Full reference: POST /api/v1/quote.

When instant pays vs. waiting

Heuristics, not rules. Quote first either way.
ScenarioRecommendation
< 50 SOL, immediate needInstant. Price impact is typically negligible.
50 to 500 SOL, time-sensitiveInstant. Quote first to confirm price impact is below your tolerance.
500 to 1000 SOL, patientSlow (deactivate). Standard path, no price impact, no fee.
1000+ SOL, time-sensitiveSplit across multiple instant unstakes over several hours, or split native:liquid. Quote each leg.
1000+ SOL, patientSlow (deactivate). The economics dominate the timeline.
For raSOL specifically, pooled liquidity is deep enough that price impact stays low further up the size curve than native depositStake. This is the practical reason raSOL is the preferred entry point for users who anticipate possibly needing instant exit.

Fees, in detail

PathHubra feeSanctum feeNetwork feeOther
Native instantNonePrice impact (dynamic)Covered by Hubra
raSOL instantNonePrice impact (dynamic)Covered by Hubra
raSOL slowNoneNoneCovered by HubraEpoch wait (~2 to 3d)
Partial native instantNonePrice impact on the sliceCovered by Hubra~0.002 SOL rent for the split account (covered)
USDC instantNoneNoneCovered by Hubra
Price impact comes from the depth of Sanctum’s pool at the moment the swap runs. Hubra adds nothing on top. The number you see in the quote is what you pay.

End-to-end example: raSOL instant unstake

import { VersionedTransaction, Keypair } from "@solana/web3.js";

// 1. Build
const build = await fetch("https://hubra.app/api/v1/unstake", {
  method:  "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    strategy: "sol-liquid-stake",
    wallet:   wallet.publicKey.toBase58(),
    amount:   "1.5",
    kind:     "instant",
  }),
}).then((r) => r.json());

const { transaction, hubra_token, route, sanctumKind, sanctum_order } = build;

// 2. Sign
const tx = VersionedTransaction.deserialize(Buffer.from(transaction, "base64"));
tx.sign([wallet]);
const signed = Buffer.from(tx.serialize()).toString("base64");

// 3. Broadcast through Sanctum (MEV-protected)
const { signature } = await fetch("https://hubra.app/api/v1/broadcast", {
  method:  "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signed_tx:     signed,
    hubra_token,
    route,
    sanctumKind,
    sanctum_order,
  }),
}).then((r) => r.json());

console.log(`Confirmed: https://solscan.io/tx/${signature}`);
For native instant, replace strategy with "sol-native-stake" and add stakeAccount. The rest of the shape is identical.

Common questions

Sanctum’s pool charges price impact for absorbing your stake or your raSOL. Hubra adds nothing on top. Larger unstakes consume more pool liquidity and incur higher impact.
0.01 SOL on the native and liquid paths. For partial native unstake, the remaining stake account must keep enough lamports to cover rent exemption (~0.002 SOL).
Single transaction. SOL arrives the moment the transaction confirms (typically a few seconds on Solana mainnet).
Slow has zero cost. If you can wait 2 to 3 days, there is no reason to pay price impact. Instant exists for the cases where waiting is not an option.
The unsigned transaction’s blockhash expires (~2 minutes), and so does the hubra_token. Rebuild via /unstake and try again. Sanctum order responses are short-lived for this reason.
Yes — route: "rpc" works for any signed transaction. You lose Sanctum’s MEV protection and smarter retries, but the on-chain effect is the same.
Yes. POST /api/v1/unstake with kind: "instant" returns the unsigned transaction; the agent signs locally and broadcasts.

Get started

Unstake in the app

Open the app, navigate to your position, choose instant.

POST /api/v1/unstake

Build an unstake transaction programmatically.