Integration example
This page walks through a complete Sui native staking flow with the Staking API (craft → sign → prepare → broadcast), including the signing step. The example uses the stake action; unstake follows the same pattern (it takes a staked_sui_object_id instead of an amount).
Signing happens entirely in your environment; the Stakely Staking API never has access to your private keys. Two signing options are shown below: a local private key and an external custodian (Fireblocks).
Set X-NETWORK to the network you want to target. Discover the values enabled for your API key with the networks endpoint.
Setup
This example targets Node.js 24 (LTS). Install the dependencies:
npm install @mysten/sui dotenv
Fill in your values in .env:
STAKELY_API_KEY=
STAKELY_API_BASE_URL=https://staking-api.stakely.io
STAKELY_NETWORK=mainnet
SUI_WALLET_ADDRESS=
SUI_PRIVATE_KEY=
A small helper to call the API with the required headers:
require('dotenv').config();
const BASE = process.env.STAKELY_API_BASE_URL;
const headers = {
'X-API-KEY': process.env.STAKELY_API_KEY,
'X-NETWORK': process.env.STAKELY_NETWORK,
'Content-Type': 'application/json',
};
// fetch is available globally in Node.js 24
const api = async (path, body) => {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} → ${res.status}: ${await res.text()}`);
return res.json();
};
1. Craft the transaction
Ask the API to build the unsigned stake transaction.
const crafted = await api('/api/v1/sui/native/action/stake', {
wallet_address: process.env.SUI_WALLET_ADDRESS,
amount: 1,
});
// crafted = { unsigned_tx_b64 }
2. Sign the transaction
Sign the unsigned_tx_b64 returned above. Use one of the two options below; both return a base64 signature you collect into a signatures array for the next steps.
Option A: Local private key (Mysten Sui SDK)
const { fromBase64 } = require('@mysten/sui/utils');
const { Ed25519Keypair } = require('@mysten/sui/keypairs/ed25519');
const signWithPrivateKey = async ({ unsigned_tx_b64 }) => {
const txBytes = fromBase64(unsigned_tx_b64);
const cleanHex = process.env.SUI_PRIVATE_KEY.replace(/^0x/, '');
const secretKeyBytes = Uint8Array.from(Buffer.from(cleanHex, 'hex'));
const keypair = Ed25519Keypair.fromSecretKey(secretKeyBytes);
const { signature } = await keypair.signTransaction(txBytes);
return signature;
};
const signature = await signWithPrivateKey(crafted);
const signatures = [signature];
Option B: External custodian (Fireblocks)
Private keys are never exposed to your application; signing is delegated to Fireblocks' secure environment. Install and configure the SDK:
npm install @mysten/sui fireblocks-sdk
FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=
SUI_FIREBLOCKS_ASSET_ID=
const { fromBase64, toBase64 } = require('@mysten/sui/utils');
const { FireblocksSDK, TransactionOperation, PeerType, TransactionStatus } = require('fireblocks-sdk');
const fireblocks = new FireblocksSDK(
process.env.FIREBLOCKS_API_SECRET,
process.env.FIREBLOCKS_API_KEY,
process.env.FIREBLOCKS_BASE_URL,
);
const waitForTxCompletion = async (fbTx) => {
let tx = fbTx;
while (tx.status !== TransactionStatus.COMPLETED) {
if (
tx.status === TransactionStatus.BLOCKED ||
tx.status === TransactionStatus.FAILED ||
tx.status === TransactionStatus.CANCELLED
) {
throw new Error(`Fireblocks signer: transaction ${tx.status}`);
}
tx = await fireblocks.getTransactionById(fbTx.id);
}
return fireblocks.getTransactionById(fbTx.id);
};
const signWithFireblocks = async ({ unsigned_tx_b64 }) => {
const txBytes = fromBase64(unsigned_tx_b64);
const contentHex = Buffer.from(txBytes).toString('hex');
const fbTx = await fireblocks.createTransaction({
assetId: process.env.SUI_FIREBLOCKS_ASSET_ID,
operation: TransactionOperation.RAW,
source: { type: PeerType.VAULT_ACCOUNT, id: process.env.FIREBLOCKS_VAULT_ID },
note: 'Sign Sui transaction from Stakely Staking API',
extraParameters: { rawMessageData: { messages: [{ content: contentHex }] } },
});
const result = await waitForTxCompletion(fbTx);
const signedMessage = result.signedMessages && result.signedMessages[0];
if (!signedMessage?.signature?.fullSig) {
throw new Error('Fireblocks did not return a usable signature for Sui');
}
// Sui expects base64(flag || signature || pubkey). Wrap the raw Ed25519 material
// returned by Fireblocks with the Ed25519 flag byte (0x00).
const fullSigBytes = Buffer.from(signedMessage.signature.fullSig, 'hex');
return toBase64(Buffer.concat([Buffer.from([0]), fullSigBytes]));
};
const signature = await signWithFireblocks(crafted);
const signatures = [signature];
3. Prepare the signed transaction (optional)
Combine the unsigned transaction and signature(s) into a single signed payload. This is useful if you want to broadcast through your own RPC.
const prepared = await api('/api/v1/sui/native/action/prepare', {
unsigned_tx_b64: crafted.unsigned_tx_b64,
signatures,
});
// prepared = { signed_tx_b64 }
4. Broadcast
On Sui, the broadcast endpoint takes the unsigned transaction and the signature(s) directly and returns the transaction hash.
const { tx_hash } = await api('/api/v1/sui/native/action/broadcast', {
unsigned_tx_b64: crafted.unsigned_tx_b64,
signatures,
});
console.log('Broadcasted:', tx_hash);
Broadcasting submits the transaction; confirm it reached finality by polling tx_hash on-chain. See Errors & responses for how to handle results and failures.
Next steps
- Full request/response schemas for every action (including
unstakeand balance queries): Endpoints. - The same four-step flow on other networks and the conceptual overview: How it works.