How an on-chain stake moves
The life of a Solana stake in the on-chain product — quote, consent, gather, delegation, observation, exit, claim and return — which scheduled task drives each step, and why the platform never credits a coin before it has landed.
The on-chain product is custodial staking with an external payer: the customer's coins are delegated on the network from a wallet this platform controls, and the reward is whatever the network pays, observed on-chain. This page follows one stake through the engine so the operator knows what each scheduled task is doing and what a screen is showing.
Everything below is driven by the five tasks under System → Cron. Nothing requires a person; the consoles exist to watch, pause intake and answer incidents.
1. Quote and consent
POST /api/staking/real/quote reads the chain and answers with the floor (the
network's minimum delegation plus the stake account's rent reserve), the gather
fee, whether the customer's own SOL deposit address holds enough on-chain, the
activation delay, the unbonding estimate and its worst case, the commission,
the share price, the custody address, the validator count and the disclosure
version. GET /api/staking/real/disclosure/:poolId is the pool page: the text
generated from the pool's rows, its version, and the five acknowledgements.
POST /api/staking/position with consent: { version, acknowledgements } places
the stake. It is refused if the version is stale or any acknowledgement is
missing. In one database transaction: the consent row (text and hash,
verbatim), the position (PENDING_DELEGATION), the ECO ledger debit, and a
GATHER batch.
An ECO balance and an on-chain balance are two facts. The ledger can say a customer holds 5 SOL while their deposit address holds 1, because a transfer inside the platform moves ledger entries, not coins. A gather is signed with the customer's own key and moves coins from their own address, so the quote refuses when that address is short and says why.
2. Gather
processRealStakingBatches (every two minutes) signs each due gather with the
customer's deposit key and moves exactly the stake amount into the staking
wallet. The transaction firewall checks it is a single transfer to the
staking wallet, paid by the sender. A gather that the address cannot pay is
retried a few ticks and then failed; a failed gather is refunded to the
ledger in full and the position ends FAILED. Nothing on-chain ever moved.
3. Delegate
The same task aggregates gathered principal per pool and splits it across the
pool's validator set by weight, each piece at or above the chain's minimum,
no validator above 15% of the pool. Each piece is a stake account derived
from the staking wallet with a seed, created, initialised with the wallet as
both authorities and no lockup, and delegated — one transaction per three
pieces, each approved by the firewall before signing. When it lands, the
tranches are ACTIVATING, the positions are ACTIVE, and shares are minted at
the current price. The stake earns from the next epoch boundary.
4. Observe
observeRealStaking (every fifteen minutes) reads, for every completed epoch
since the pool was last observed, the inflation reward the protocol credited
to each stake account. One observation row per epoch, unique. The share price
moves by the reward; the commission is minted as shares to the pool's treasury
on a positive reward only; one COMPOUNDED earning row per active position
records what the network paid that holder. Then the book is squared against
the chain: any residual the epochs do not explain becomes its own window,
negative if it must be, with no commission — and a negative window opens a
SLASHING incident.
The rent reserve inside every stake account was paid by the staking wallet and returns to it on withdrawal; it is never counted as holders' value.
A commission increase saved on the pool applies from the first window after its notice date; a decrease applied when it was saved.
5. Unstake
POST /api/staking/position/:id/unstake (optionally with shares) is accepted
at any time from ACTIVE. It cannot be cancelled. The share price freezes at
the request and rewards stop at the request. A partial unstake splits the row,
so every row settles at one price. The admin's only action on an on-chain
position is POST /api/admin/staking/position/:id/force-unstake with a
reason; rewards to the request are still paid.
6. Exit, claim, return
The batch task walks the pool's tranches oldest first. A tranche is split
and the split deactivated when both sides stay at or above the minimum
delegation; otherwise the whole tranche is deactivated and the surplus beyond
what the exits needed returns to the pool's undelegated value and is swept into
a new tranche when it reaches a stake account's minimum. Positions move to
UNBONDING with an estimate and a worst-case bound.
Once a stake account is inactive (its deactivation epoch has passed), a
CLAIM withdraws it to the staking wallet. Positions whose tranches are all
withdrawn settle at min(request price, current price) — the rule every
large exchange and both liquid-staking protocols publish — burn their shares
and become WITHDRAWABLE. A RETURN then transfers each settled amount to the
customer's own deposit address.
The ledger credit is written when the return transaction is confirmed, and with the deposit watcher's own idempotency key for that transaction. The ecosystem watcher will see the same transfer arrive at the customer's address and try to credit it too; whichever of the two runs second is refused. There is no second credit, and there is no ledger-only return that would back a spendable balance with coins still in the staking wallet.
Ethereum, through Lido
The same doors and the same scheduled tasks serve an ETH pool; only the
venue differs. The pool's venue picks the engine.
- Gather moves ETH from the customer's own deposit address (an HD child of the ecosystem master wallet, whose key the ecosystem stores) into the staking wallet, signed with the customer's key, gas paid by their address.
- Submit is
stETH.submitfrom the staking wallet, once for every gathered position per tick, and only when Lido's stake limit allows the amount and staking is not paused — a submit the limit refuses is deferred, not failed. The shares Lido mints are read from theTransferSharesevent and become the pool's single stETH tranche; pool shares are minted at the current price. - Observe runs once per UTC day: the value of the pool's stETH shares is
getPooledEthByShares, the figure Lido itself publishes. A negative rebase is a negative window with no commission and aSLASHINGincident. Bunker mode raises the estimate and the bound the quote shows; it never stops an exit. - Exit is two transactions:
approveto the withdrawal queue, thenrequestWithdrawalsin chunks of at most 1,000 stETH and at least the queue's minimum. The request ids come from the receipt and sit on the batch. Claim waits until every request of the pool's exit is finalised, reads the checkpoint hints and callsclaimWithdrawals; the ETH that lands is the pool's unallocated value and queued positions settle at min(request price, current price). Return is a plain transfer to the customer's address, credited with the deposit watcher's own key. - Every Ethereum transaction is type 2 (EIP-1559), signed before its hash is recorded on the batch with its nonce, and broadcast once; a rebuild reuses the nonce so a late-landing first attempt replaces rather than duplicates. The firewall allow-lists the contract address and the four-byte selector per network against Lido's registry (mainnet and Hoodi; Lido's Sepolia deployment is deprecated and is not offered).
The Ethereum provider is built from ETH_MAINNET_RPC / ETH_HOODI_RPC — the
same variables the ecosystem reads — pinned to the network's chain id.
What the staker sees
Web. An on-chain pool's page is the disclosure: the facts generated from its rows (custody address, network, validators, commission, activation delay, exit estimate and bound, the observed trailing rate labelled as history), the disclosure text with its version, the quote for the typed amount, and the five acknowledgements. The stake button sends the quoted amount with the consent; a disclosure that changed under the reader is refused with 409 and reloaded, with every tick cleared. The pool card shows "Observed, last 30 days" where a fixed-rate card shows its APR, and "Exit takes" where it shows the lock. A position card walks the on-chain states — Delegating, Staked, Exit requested, Unbonding, Returning, Returned, Failed — and offers Unstake (all of it, or a number of shares) on a Staked one. There is no withdraw, no claim and no early exit; the fixed-rate routes refuse a REAL row by name. The Guide page has an "On-chain pools" section saying the same in plain words.
Mobile. The app parses mode and the on-chain fields, gates every rate
and lock display off for a REAL pool, renders the pool's hero as "On-chain ·
x% observed", and replaces the stake form with the same disclosure → quote →
acknowledgements → stake flow. The position card shows value now, rewards to
date and the "back by" date once an exit is requested, and its Unstake button
dispatches the on-chain exit. A state the build does not know reads as
"Processing", never as the raw enum. The native response strip still removes
trailingRewardRateBps, so on a store build the observed figure reads "Not
shown in this app" rather than a number — a native build never leads with a
rate, historical or not.
Landing and stats. Average, highest and lowest APR, the "highest APR" and
"flexible" pool lists and the per-token aggregates count fixed-rate pools
only: an on-chain pool's apr column is 0 and its lockPeriod is 0 because
it has neither, not because they are low. On-chain capital still counts
toward what is staked. A promoted on-chain pool's featured card says "What
the network pays" and "Exit takes" instead of a rate and a lock.
What the batch ledger shows
Every step above is a row on Batches with the intent the firewall
approved, its digest, the signature, and the state. A batch that reached the
chain is never marked failed; it retries with backoff until it lands, and the
runner asks the cluster whether a signature landed before it ever rebuilds a
transaction. The firewall refuses, by name, any instruction outside create,
transfer, initialize, delegate, deactivate, withdraw, split and merge — in
particular authorize and setLockup, the two that can change who controls
a stake.
What is not automatic
- Funding the staking wallet's gas reserve (a
LOW_GASincident says when). - Answering incidents, including the capped reimbursement on a slashing.
- Choosing validators (screened for you; chosen by you).
- Deciding the commission, and any territory beyond the derived defaults.
- Realising the commission: queuing the treasury's shares for exit. The engine then covers, settles and pays it like any user's exit, users first.
Everything else — gathering, delegating, observing, exiting, claiming, returning, refunding, reconciling, alerting, and the monthly statements — runs on the scheduled tasks. Operating on-chain staking is the runbook.