Staking
Requires pools:read for the reads and transactions:write for the
transaction flow - granted by name, or through the
all:read / all:write wildcards.
Finding a pool
Code
Returns the curated selection split into low / mid / high by risk score,
ordered by rank within each bucket. Each pool carries its slug - the handle
every other call takes - along with apy, avg30dApy, tvlUsd, feePercent,
riskScore, and the tokens involved:
Code
stakingEnabled: false means the pool is listed for data only: building a stake
or an unstake against it is a 400 - check it before building a transaction. updatedAt is when the
stats were last recomputed, which is what to key your own caching on.
Exit liquidity (utilization)
A lending pool pays depositors back out of what is not lent out. At 100%
utilization every unit supplied is borrowed, so an unstake reverts until
borrowers repay or new supply arrives - while the APY is at its highest.
utilization tells you how close a pool is:
| Field | Meaning |
|---|---|
rate | Percent of depositor assets that cannot be withdrawn right now, 2 dp |
availableLiquidity | What can be withdrawn right now, in base-token units |
level | normal, high (from 90%) or critical (from 98%). A pool leaves a level only 3 points below its threshold, so it does not flap |
observedAt | When it was read. Readings are hourly |
For a vault the rate is exit utilization: the share of its assets it cannot
pay out now, whatever the vault's own reporting calls utilization.
utilization is null for pools we do not monitor (DEX pools, and vaults we
have not mapped yet) and for pools not read yet. Consider holding new stakes
into a critical pool, and warn users before an unstake from one. A change of
level is also pushed to you as the
staking_pool_utilization_changed callback.
Which pools you see
Every pool we list is available to you by default, new ones included - there is
nothing to opt into. Your team can switch individual pools off in the partner
panel, and a pool switched off leaves this list and stops accepting
transactions: a chart, stake or unstake addressed at its slug answers 403.
That is your own configuration, not a permission we set, so the fix is a toggle in the panel rather than a support ticket. A change takes effect within a minute across every instance.
History for one pool:
Code
Ascending by date, defaulting to the last 7 days when dateFrom is omitted.
Points with a zero APY are left out.
The transaction flow
Staking is two steps, because the API never holds a user's key:
POST …/construct/stake/transaction/batchreturns an unsigned EIP-7702 batch user operation plus anintentId.- The user's wallet signs
bundle.userOpHashand, whenbundle.authorizationis notnull, the EIP-7702 authorization tuple. POST …/transaction/batch/sendreturns the signatures with theintentId, and the API submits the operation.
The user operation itself never travels back to you. It stays server-side under
the intentId, which is what stops a signed call from being replayed against a
different chain, amount or target than the one that was quoted.
1. Build
Code
Code
userId names the user the transaction acts for - a partner token identifies
your organisation, not a person, so the subject has to be explicit. It must be
one of your users; anything else is a 404.
amount in the response is the amount the batch was built for.
Gas is always sponsored: the user's address never needs the chain's native token. The platform fee includes a gas-coverage component that pays for it, priced from the chain's current gas price.
Building for the largest amount (maxAmount)
A "max" button should not send the wallet balance: the platform fee comes on
top of a USDC stake, and an unstake can be capped by what the pool can pay out.
Send "maxAmount": true instead of amount, and the batch is built for the
largest amount the wallet can afford right now, fee included. The response's
amount says what that was.
Code
- Stake: the base-token balance less the platform fee, when the fee is paid in the same token. A tiny remainder stays in the wallet, so a gas price that rises before the batch lands cannot make it unaffordable.
- Unstake: every share the wallet holds that the pool can pay out now (ERC-4626
maxRedeem, or the Aave reserve's unborrowed liquidity). The fee comes out of the withdrawal. A DEX pool answers400(MAX_AMOUNT_NOT_SUPPORTED). amountis ignored whenmaxAmountistrue.
2. Sign
The wallet produces one or two signatures:
signature- EIP-191 over the 32 bytes ofbundle.userOpHash, prefixed with0xff00.authorization- the EIP-7702 tuple signed as{ r, s, yParity }, usingbundle.authorization(chainId,address,nonce). Only whenbundle.authorizationis notnull.
bundle.authorization is null once the wallet already delegates to
bundle.delegate - normally every batch after the user's first on that chain.
Then sign only userOpHash and leave authorization out of the submit body.
Sending one anyway, or leaving it out when the bundle has one, is a 400
(AUTHORIZATION_INVALID).
Signing with Privy and Signing with Wallet Core cover both signatures in code, along with the mistakes that get a submit rejected.
3. Submit
Code
Code
An intent is single-use and short-lived. A replayed or late submit is a
400 - rebuild and re-sign rather than retrying the same intentId.
Estimating the fee (fee-estimate)
GET …/fee-estimate/:slug?action=stake|unstake&amount=… answers the platform
fee and gas fee on an amount, and what is left of it once they are taken. It
reads no wallet and builds nothing, so you can call it as the user types. It
needs pools:read, and refuses a pool you switched off with 403, like a
build.
Code
Code
actionisstake(amountin base-token units) orunstake(share units), matching the construct call it is for.feePercentis the tier the amount falls in - from the fee tiers you set for your users in the partner panel (Fees), or the platform's default tiers if you have not set any. A change there applies to every estimate and batch built after it.gasFeeUsdis the gas coverage at the chain's current gas price; it moves with the network.finalAmountis what is left once the fee is taken. On a stake it isamountless the fee, so for a wallet holdingamountit is about whatmaxAmountwill stake. On an unstake it is what the shares redeem for (finalAmountTokenis the base token) less the fee. When the fee is paid in another token, nothing is taken from it.- The figures are approximate. The construct call charges the exact fee, and refuses an amount the wallet cannot cover.
Unstaking, and the units trap
Unstaking is the same three steps against
…/construct/unstake/transaction/batch, with one difference that is easy to get
wrong:
| Direction | amount is denominated in |
|---|---|
| Stake | the base token - baseToken.decimals |
| Unstake | the wrap token (shares) - wrapToken.decimals |
A withdrawal redeems the share token the deposit minted, so it is denominated in
that token, not in the asset the user originally deposited. The two can have
different decimals, and their exchange rate drifts as the position accrues -
which is exactly why the pool view carries decimals for both.
amount is a human-readable number (250.5), not base units; the API scales it
by the decimals of the correct token.
An unstake batch also revokes any base-token allowance the wallet still grants
the pool contract, so bundle.calls may carry an approve(pool, 0) right after
the withdrawal - see Allowances are revoked on unstake.
Unstaking a full position
Read the user's share balance and unstake that, rather than converting the staked amount back through an exchange rate you computed yourself. The rate moves between your read and the transaction landing.
Failure modes
| Status | Cause |
|---|---|
400 | Unknown pool slug, the pool has stakingEnabled: false, the user has no linked wallet, neither amount nor maxAmount was sent, the wallet cannot afford amount plus fee (INSUFFICIENT_BALANCE), an unstake above what the pool can pay out now, or the intent is unknown, already used or expired |
403 | Your token lacks transactions:write (or pools:read on fee-estimate), or your team has switched this pool off in the panel |
404 | No such user, or the user belongs to another partner |
