Stake OPEN for a role
Almost every role beyond buying does something that could hurt somebody else if done carelessly, so those roles put OPEN at risk before they get to do it. Staking is that deposit. It stays yours, and it can be cut if you misbehave. It does not pay you anything today. The reward calculation now exists and nodes publish the liveness observations it runs on, but nobody is running the submission step on devnet and the rewards vault is empty, so no stake has yet earned a token.
मील का पत्थर 1 / 6 · Run the infrastructure
शुरू करने से पहले
- A Solana wallet on devnet, holding some SOL for transaction fees
- OPEN in that wallet as a Token-2022 account — see the step below on where OPEN comes from
- The role you intend to take: one stake account per (wallet, role), so decide before you open one
- For anything past staking itself — unbonding, withdrawing, claiming rewards — a script using the SDK, since only staking has a browser flow today
चरण
- 01
What stake is, and what it is not
Stake is a deposit you keep ownership of, not a fee you pay to anyone. It is not reputation either, and it cannot be turned into reputation: an operator who performs badly cannot make up for it by staking more, because reputation is earned from completed work and tracked separately. What stake actually buys is skin in the game — a number the protocol can reduce if you break a rule, which is what makes strangers willing to rely on you.
- 02
Choose the role you are staking for
There are seven staked roles, and a stake account is keyed by (owner, role) — so one wallet can hold completely independent stakes under several roles at once, each with its own balance and its own unbonding timer. The role is fixed when you open the account; to move stake between roles you unbond from one and stake into the other.
The seven roles, and their on-chain valuesMerchant = 0 Arbitrator = 1 NodeOperator = 2 NotificationProvider = 3 OracleProvider = 4 RiskIntelligenceProvider = 5 SnapshotProvider = 6 // The stake account's address is derived from the pair, so the role is // part of the account's identity rather than a field you can change: // seeds = ["stake", owner_pubkey, role_as_u8] - 03
Get OPEN into your wallet
The devnet OPEN mint's mint authority is permanently unset — the full billion-token supply was minted once at genesis and nobody, including the project, can ever create more. So there is no faucet and no way to mint yourself test OPEN: on devnet it has to be sent to you by someone already holding it from the genesis distribution. Your wallet also needs a Token-2022 account for the mint before it can receive any.
shell# The devnet OPEN mint. Note "Mint authority: (not set)" — the supply # is fixed at 1,000,000,000 OPEN (9 decimals) and cannot grow. spl-token display 29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj --url devnet # Your own balance for that mint, once somebody has sent you some. spl-token balance 29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj --url devnet - 04
Open a stake account and fund it
Two instructions: one creates the account for your (wallet, role) pair, the other moves OPEN from your token account into the program's vault. The account only needs creating once, so a real client checks whether it already exists and sends just the stake instruction if so. The easiest route is the Stake page in the OpenFiat app, which connects your wallet and does exactly this — though its form currently offers only merchant, node operator, arbitrator and notification provider, so the other three roles need the SDK.
TypeScriptimport { PublicKey, Transaction } from "@solana/web3.js"; import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; import { onchain } from "@openfiat/sdk"; const mint = new PublicKey("29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj"); const role = onchain.Role.NodeOperator; // Where your OPEN actually sits. Token-2022, not the older token program. const from = getAssociatedTokenAddressSync(mint, owner, false, TOKEN_2022_PROGRAM_ID); // OPEN has 9 decimals, so amounts are in units of 1e-9 OPEN. const amount = 1_000n * 1_000_000_000n; // Skip initializeStakeAccountIx if this (wallet, role) already has one — // the instruction creates the account and will fail if it exists. const instructions = [ onchain.staking.initializeStakeAccountIx(owner, role), onchain.staking.stakeIx(owner, mint, role, from, amount), ]; const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); const tx = new Transaction({ feePayer: owner, blockhash, lastValidBlockHeight }).add(...instructions); const { signature } = await wallet.signAndSendTransaction(tx); await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");Rust — note the argument order differs from TypeScriptuse openfiat_sdk::onchain::{staking, Role}; let role = Role::NodeOperator; let amount = 1_000 * 1_000_000_000u64; // 1,000 OPEN, 9 decimals let instructions = vec![ staking::initialize_stake_account_ix(&owner, role), staking::stake_ix(&owner, role, &mint, &from, amount), ]; - 05
Read the real limits, do not trust a number in a document
Minimums, the unbonding period and the slash percentage all live in a single on-chain StakingConfig account, and governance can change them — so the authoritative values are the ones you read, not the ones written here. Worth knowing: the program stores both minimums but does not currently enforce either, so a stake below the stated minimum will be accepted on-chain today. Treat them as the bar you are expected to meet, not a guard rail that will stop you.
The values deployed on devnet at the time of writingmin_stake 1,000 OPEN # all roles min_stake_arbitrator 10,000 OPEN # arbitrators, a higher bar unbonding_period_secs 604,800 # 7 days slash_bps 1,000 # 10% of the active stake # Read them yourself rather than trusting the above — the account is a # singleton PDA, so its address is fixed: # seeds = ["staking_config"] program HYEXk8XQukBkZbiYB33JyVefQDxqyCpPudad3wBCyYmxTypeScriptimport { onchain } from "@openfiat/sdk"; const [stakingConfig] = onchain.staking.stakingConfigPda(); const account = await connection.getAccountInfo(stakingConfig); // The SDK builds instructions but does not decode accounts yet. The // layout after the 8-byte discriminator is: // admin(32) mint(32) min_stake(8) min_stake_arbitrator(8) // unbonding_period_secs(8) slash_bps(2) ... // openfiat-app's lib/onchain-decode.ts has a working decoder to copy. - 06
Unbond when you want out
Requesting an unstake moves the amount out of your active stake immediately, before you get the tokens back — deliberately, so nobody can request an unbond and keep voting with, or claiming eligibility from, stake they are already withdrawing. The tokens themselves stay locked until the unbonding period elapses. One catch worth planning around: a stake account tracks a single unbonding batch, so a second request merges into the first and pushes the release time out to a fresh full period from that moment.
TypeScriptimport { onchain } from "@openfiat/sdk"; // There is no browser flow for this yet — unbonding, withdrawing and // claiming rewards are SDK-only today. const ix = onchain.staking.requestUnstakeIx(owner, role, 500n * 1_000_000_000n);Rustuse openfiat_sdk::onchain::staking; let ix = staking::request_unstake_ix(&owner, role, 500 * 1_000_000_000u64); - 07
Withdraw once the period is up
Withdrawing takes no amount: it returns the whole unbonding balance at once, and only once the release time has passed — earlier attempts fail rather than paying out partially. You choose the destination token account, which must be for the OPEN mint.
TypeScriptconst ix = onchain.staking.withdrawUnstakedIx(owner, mint, role, destinationTokenAccount);Rustlet ix = staking::withdraw_unstaked_ix(&owner, role, &mint, &destination_token_account); - 08
Rewards accrue; slashing subtracts
No rewards have been distributed to anyone yet, though the gap is narrower than it was. The calculation is real: a node records which peers it heard from across an epoch, and a share is computed from stake weighted by connectivity and availability, paying a node genuinely connected to Solana RPC more than one only gossiping. What is missing is the last two steps — nobody runs the submission on devnet, and no instruction funds the rewards vault, whose balance is zero. So every pending balance is zero and claiming fails. Treat staking as something you do to be allowed to act, not to earn. When rewards do arrive they sit as a pending balance until claimed, so they never quietly change the stake figure your role eligibility is measured against. Slashing runs the other way: a slashing authority can cut a fixed percentage of your active stake, the forfeited tokens go to a treasury rather than being burned, and the total is recorded permanently on your account. Only your active stake is exposed — anything already unbonding is not.
TypeScript// Claiming pays out the whole pending balance. Today that balance is always // zero for everyone, because nothing distributes rewards yet, so this fails. const ix = onchain.staking.claimRewardsIx(owner, mint, role, destinationTokenAccount);Rustlet ix = staking::claim_rewards_ix(&owner, role, &mint, &destination_token_account);