Marinade Native: SDK for stake-authority automation logoMarinade Native: SDK for stake-authority automation
Create or authorize stake accounts so Marinade can rebalance them. Keep withdraw authority. Prepare an unstake, wait one epoch, then withdraw.
devrels.xyz/a/318Marinade Native is Marinade's stake automation that never mints a liquid token. SOL sits in ordinary Solana stake accounts. Marinade's bot holds stake authority so it can split, merge, and redelegate. Your wallet keeps withdraw authority, so only you can take the SOL out.
The builder surface is @marinade.finance/native-staking-sdk, currently 1.3.4. It builds the stake-program instructions, talks to Marinade's Native Staking API when you want a merged unstake, and reads which of your accounts are still staking versus ready to withdraw.
This piece is that SDK. How to stake, how to move an existing stake account in, how to ask Marinade to pack an exit, and which hosts the package actually calls.
What changes on the stake account
A Solana stake account has two authorities. Stake authority signs delegate, deactivate, split, merge, and a new stake authority. Withdraw authority signs withdraw and can also replace either authority. Marinade Native takes only stake authority.
The Max Yield stake authority is a PDA, stWirqFCf2Uts1JBL1Jsd3r6VBWhgnpdPxCTe1MFjrq. An exit authority, ex9CfkBZZd6Nv9XdnoDmmB45ymbu4arXVk7g5pWnt3N, is used while Marinade is packing accounts you asked to leave. Both PDAs come from the Native Staking proxy mnspJQyF1KdDEs5c6YJPocYdY1esBgVQFufM2dY9oDk. Marinade's council can rotate the bot that is allowed to CPI through that proxy. There is no private key sitting in a hot wallet for those PDAs.
Once Marinade has stake authority it splits the deposit across many validators (docs say over 100) and rebalances through the Stake Auction Market. Inflation rewards land in those stake accounts at epoch boundaries. There is no deposit fee and no ongoing management fee on Native.
Two configs
Construct NativeStakingSDK with a config object. Skip the config and the package still defaults stake authority to Max Yield, but RPC is http://127.0.0.1:8899. Pass your connection.
| Knob | Max Yield (default) | Marinade Select |
|---|---|---|
| Class | NativeStakingConfig | NativeStakingSelectConfig |
| Stake authority | stWirqFC…Fjrq | STNi1NHD…yGps |
| Exit authority | ex9CfkBZ…t3N | EX1Fs34a…GpH6 |
| API | native-staking.marinade.finance | ns-prime.marinade.finance |
| Fee payer (revoke) | opNS8ENp…ezP | opiNSvKm…W1L |
Select is the curated validator set. Same SDK class, different authorities and API host. The npm README's Select snippet imports the wrong config name. Use NativeStakingSelectConfig from the published types.
import { Connection } from "@solana/web3.js"
import {
NativeStakingConfig,
NativeStakingSDK,
} from "@marinade.finance/native-staking-sdk"
const config = new NativeStakingConfig({
connection: new Connection("https://api.mainnet-beta.solana.com"),
})
const sdk = new NativeStakingSDK(config)Stake SOL
buildCreateAuthorizedStakeInstructions builds aStakeProgram.createAccount. Stake authority is Marinade. Withdraw authority is the user. The new stake account keypair must sign. Minimum amount is 2,282,880 lamports, the frozen stake-account rent the SDK hardcodes as STAKE_RENT_EXEMPT.
import {
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js"
import BN from "bn.js"
const amount = new BN("1000000000") // 1 SOL
const { createAuthorizedStake, stakeKeypair } =
sdk.buildCreateAuthorizedStakeInstructions(publicKey, amount)
const { blockhash } = await connection.getLatestBlockhash()
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: publicKey,
recentBlockhash: blockhash,
instructions: createAuthorizedStake,
}).compileToV0Message(),
)
tx.sign([stakeKeypair])
await signTransaction(tx)
await sendTransaction(tx, connection)
await sdk.callRebalanceHint(publicKey)callRebalanceHint is optional. It POSTs the user pubkey to Marinade so a bot can pick the new account up before the usual end-of-epoch pass. If you skip it, Marinade still notices the account. Delegation often waits until later in the epoch, so a brand-new account can sit undelegated for a while. That is expected.
Move an existing stake account
If the wallet already has a delegated stake account, you do not unstake first. buildAuthorizeInstructions sets stake authority to Marinade and leaves withdraw authority alone. The current stake authority has to sign, which is usually the same wallet.
const authorizeInstructions = sdk.buildAuthorizeInstructions(
publicKey,
[stakeAccount],
)
const { blockhash } = await connection.getLatestBlockhash()
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: publicKey,
recentBlockhash: blockhash,
instructions: authorizeInstructions,
}).compileToV0Message(),
)
await signTransaction(tx)
await sendTransaction(tx, connection)Referral codes
A referral code is a pubkey. Two ways to attach it, both in the same package.
On-chain memo: buildReferralInstructions(partnerPubkey) returns a memo instruction whose JSON is {"code":"<pubkey>"}. Prepend it to the create or authorize instructions and send one transaction.
Hosted transaction: getRefSolSignedTransaction(user, amount, code) and getRefStakeAccountSignedTransaction(user, stake, code) GET a serialized versioned transaction from https://ns-referral.marinade.finance at /v1/tx/deposit-sol or /v1/tx/deposit-stake-account. The code string must be a valid pubkey or the SDK throws before it fetches.
@marinade.finance/marinade-ts-sdk 6.1.0 re-exports wrappers named getRefNativeStakeSOLTx and getRefNativeStakeAccountTx. Those wrappers hit https://native-staking-referral.marinade.finance, a different host than the dedicated Native SDK. If you are integrating Native, import @marinade.finance/native-staking-sdk and use ns-referral.marinade.finance.
Leave: pay, wait, withdraw
You can always take stake authority back with Solana CLI and deactivate each account yourself. Marinade splits deposits across many validators, so that is painful. The SDK path pays a small fee, asks Marinade to deactivate and merge, then you withdraw one account after the epoch cooldown.
initPrepareForRevoke(user, amount) returns payFees and onPaid. payFees is a SOL transfer to the config beneficiary plus a memo PrepareForRevoke that names the user and amount. Pass null or omit amount to prepare everything. Docs still describe this fee as 0.001 SOL. The published SDK default prepareForRevokeCost is 3,000,000 lamports (0.003 SOL). Read the value off sdk.config.prepareForRevokeCost rather than hardcoding either number.
const amount = new BN("1000000000")
const { payFees, onPaid } = await sdk.initPrepareForRevoke(
publicKey,
amount,
)
const { blockhash } = await connection.getLatestBlockhash()
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: publicKey,
recentBlockhash: blockhash,
instructions: payFees,
}).compileToV0Message(),
)
await signTransaction(tx)
const signature = await sendTransaction(tx, connection)
await connection.confirmTransaction(signature, "finalized")
await onPaid(signature)onPaid POSTs the signature to Marinade so a bot can start deactivating. At the next epoch boundary Marinade merges the cooled-down accounts. Then getStakeAccounts(user) moves them from preparingToRevoke into readyToRevoke. Withdraw with the stake program, or call buildRevokeInstructions first if you want stake authority back on the user before you pull the lamports.
Locked stake is supported. When only part of the position is unlocked, Marinade spends unlocked accounts first, then the shortest remaining lockups.
Read the position
getStakeAccounts(user) loads accounts where withdraw authority is the user and stake authority is either the staking PDA or the exit PDA. It returns four arrays: all, staking, preparingToRevoke, readyToRevoke. Ready means the current epoch has passed the deactivation epoch.
fetchRewards(user) GETs epoch balances and inflation rewards. apy_5_epochs is null until there is an active stake to compute against. The Staking Rewards Report in the Marinade app covers Max Yield and Select paid in SOL. Recipes (rewards paid in another token) are a different path and are not in that report.
What the SDK calls on the wire
Default API host is native-staking.marinade.finance. The 1.3.4 client still uses /v1/... paths. The OpenAPI at /docs.json documents a /v2/{stake_authority}/... surface. Both /v1/user-rewards and the v2 twin return JSON today. Pin what the SDK sends unless you are calling the API yourself.
POST /v1/prepare-for-revokewith{ signature, user, amount? }POST /v1/rebalance-hintwith{ user }GET /v1/user-rewards?user=- Referral txs from
ns-referral.marinade.financeas above
Dev hosts exist in the package: native-staking-dev.marinade.finance and ns-prime-dev.marinade.finance. Override nativeStakingApiUrl on the config if you need them.
If the app is gone
The SOL never left the Stake program. Marinade published how-to-native-staking (archived) for a CLI-only exit. List accounts with solana stakes --withdraw-authority <you>. An undelegated account withdraws in one instruction. A delegated one needs stake-authorize back to you, then deactivate-stake, then a wait until the epoch ends, then withdraw-stake.
The marinade-ts-sdk wrappers
marinade-ts-sdk 6.1.0 depends on this package and re-exports getAuthNativeStakeSOLIx, getAuthNativeStakeAccountIx, getPrepareNativeUnstakeSOLIx, and callRebalanceHint. Those helpers construct new NativeStakingSDK() with no connection. Fine for instruction-only helpers. For getStakeAccounts or anything that hits RPC, construct the Native SDK yourself with NativeStakingConfig.
That same ts-sdk also has the liquid-staking Marinade class (deposit, liquidUnstake). That is mSOL. Keep it on a different code path from Native.
People and links
| What | Where |
|---|---|
| Product | marinade.finance |
| Native overview | docs.marinade.finance Marinade Native |
| API and SDK | Native API and SDK |
| OpenAPI | native-staking.marinade.finance/docs |
| npm | @marinade.finance/native-staking-sdk |
| TS SDK (wrappers) | marinade-finance/marinade-ts-sdk |
| CLI exit notes | marinade-finance/how-to-native-staking |
| Contract addresses | developers/contract-addresses |
| X | @MarinadeFinance |
| GitHub org | marinade-finance |
Keep reading
Solana's TypeScript ecosystem fractured: web3.js v1, then the Kit rewrite, then Gill and Kite wrappers — and choice paralysis. web3.js 3.0 is the convergence move: the API everyone already knows, rebuilt on Kit's modern internals. What it actually is, with the package facts the announcement leaves out.
Update every @dynamic-labs/* package together. sdk-react-core 5.7.0. New React apps can use @dynamic-labs-sdk/react-hooks. Embedded-wallet showdown stays.
Public packages @svsprotocol/solana 0.6.0, create-svs-agent, svs-solana. Demo registry is devnet. No completed third-party audit yet.
Get new articles in your inbox
Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.
