Skip to main content

Integration example

This page walks through a complete JPool liquid staking flow with the Staking API (craft → sign → prepare → broadcast), including the signing step. The example uses the stake action (depositing SOL and receiving the JSOL pool token); unstake follows the same pattern.

JPool runs on Solana, so signing is the same as Solana native staking. 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 cluster you want to target (mainnet-beta or devnet). 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 @solana/web3.js bs58 dotenv

Fill in your values in .env:

STAKELY_API_KEY=
STAKELY_API_BASE_URL=https://staking-api.stakely.io
STAKELY_NETWORK=mainnet-beta
SOL_WALLET_ADDRESS=
SOL_WALLET_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 JPool stake transaction. The minimum deposit is 0.003 SOL.

const crafted = await api('/api/v1/solana/jpool/action/stake', {
wallet_address: process.env.SOL_WALLET_ADDRESS,
amount: 0.1,
});
// crafted = { unsigned_tx_hex }

2. Sign the transaction

Sign the unsigned_tx_hex returned above. Use one of the two options below; both return an array of hex signatures that the prepare step expects.

Option A: Local private key

const web3 = require('@solana/web3.js');
const bs58 = require('bs58');

// Handles both legacy and versioned transactions
const getRawTransaction = (encodedTransaction) => {
try {
return web3.Transaction.from(Buffer.from(encodedTransaction, 'hex'));
} catch {
return web3.VersionedTransaction.deserialize(Buffer.from(encodedTransaction, 'hex'));
}
};

const signWithPrivateKey = ({ unsigned_tx_hex }) => {
const tx = getRawTransaction(unsigned_tx_hex);
const keypair = web3.Keypair.fromSecretKey(bs58.decode(process.env.SOL_WALLET_PRIVATE_KEY));

let signatureBytes;
if (tx instanceof web3.VersionedTransaction) {
tx.sign([keypair]);
signatureBytes = tx.signatures[0];
} else {
tx.partialSign(keypair);
signatureBytes = tx.signature;
}

return [Buffer.from(signatureBytes).toString('hex')];
};

const signatures = signWithPrivateKey(crafted);

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 @solana/web3.js fireblocks-sdk
FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=
const web3 = require('@solana/web3.js');
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 getRawTransaction = (encodedTransaction) => {
try {
return web3.Transaction.from(Buffer.from(encodedTransaction, 'hex'));
} catch {
return web3.VersionedTransaction.deserialize(Buffer.from(encodedTransaction, 'hex'));
}
};

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: the transaction has been ${tx.status}`);
} else if (tx.status === TransactionStatus.REJECTED) {
throw new Error('Fireblocks signer: the transaction was rejected; check your TAP security policy');
}
tx = await fireblocks.getTransactionById(fbTx.id);
}
return fireblocks.getTransactionById(fbTx.id);
};

const signWithFireblocks = async ({ unsigned_tx_hex }) => {
const tx = getRawTransaction(unsigned_tx_hex);

// Fireblocks RAW signing expects the serialized message bytes as hex
const messageBytes = tx instanceof web3.VersionedTransaction
? tx.message.serialize()
: tx.serializeMessage();

const fbTx = await fireblocks.createTransaction({
assetId: 'SOL', // use 'SOL_TEST' for Fireblocks testnet
operation: TransactionOperation.RAW,
source: { type: PeerType.VAULT_ACCOUNT, id: process.env.FIREBLOCKS_VAULT_ID },
note: 'Sign transaction from Stakely Staking API',
extraParameters: { rawMessageData: { messages: [{ content: Buffer.from(messageBytes).toString('hex') }] } },
});

const result = await waitForTxCompletion(fbTx);
const signatures = [];
result.signedMessages?.forEach((signedMessage) => {
if (signedMessage.derivationPath[3] === 0) {
signatures.push(signedMessage.signature.fullSig);
}
});
return signatures;
};

const signatures = await signWithFireblocks(crafted);

3. Prepare the signed transaction

Send the crafted unsigned_tx_hex with the signature(s) to get the final signed transaction.

const prepared = await api('/api/v1/solana/jpool/action/prepare', {
unsigned_tx_hex: crafted.unsigned_tx_hex,
signatures,
});
// prepared = { signed_tx_hex }

4. Broadcast

Submit the signed transaction. You can use the Stakely broadcast endpoint or your own RPC.

const { tx_hash } = await api('/api/v1/solana/jpool/action/broadcast', {
signed_tx_hex: prepared.signed_tx_hex,
});

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.