Skip to main content

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).

Test on testnet first

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 unstake and balance queries): Endpoints.
  • The same four-step flow on other networks and the conceptual overview: How it works.