Skip to main content

Integration example

This page walks through a complete Monad staking flow with the Staking API (craft → sign → prepare → broadcast), including the signing step. The example uses the delegate action, but the same pattern applies to undelegate, withdraw, claim-rewards, and compound: only the endpoint and request body change.

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). Monad is EVM-compatible, so standard Ethereum tooling applies.

Test on testnet first

Set X-NETWORK to the chain you want to target (143 for Monad mainnet). 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 ethereumjs-util dotenv

Fill in your values in .env:

STAKELY_API_KEY=
STAKELY_API_BASE_URL=https://staking-api.stakely.io
STAKELY_NETWORK=143
EVM_CHAIN_ID=143
ETHEREUM_WALLET_ADDRESS=
ETHEREUM_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 delegate transaction.

const crafted = await api('/api/v1/monad/native/action/delegate', {
address: process.env.ETHEREUM_WALLET_ADDRESS,
amount: 0.1,
});
// crafted = { serialized_tx_hex, unsigned_tx_hash_hex, payload }

2. Sign the transaction

Sign the unsigned_tx_hash_hex returned above. Use one of the two options below; both return the EIP-155 signature parts { r, s, v } that the prepare step expects.

Option A: Local private key

const ethUtil = require('ethereumjs-util');

const signWithPrivateKey = ({ unsigned_tx_hash_hex }) => {
const txHashBytes = Buffer.from(unsigned_tx_hash_hex.replace(/^0x/, ''), 'hex');
const privateKeyBuffer = Buffer.from(process.env.ETHEREUM_WALLET_PRIVATE_KEY, 'hex');

// ecsign with the chain id produces the EIP-155 v the API expects
const { r, s, v } = ethUtil.ecsign(
txHashBytes,
privateKeyBuffer,
Number(process.env.EVM_CHAIN_ID),
);

return { r: r.toString('hex'), s: s.toString('hex'), v };
};

const { r, s, v } = 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 fireblocks-sdk
FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=
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: 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_hash_hex }) => {
const content = unsigned_tx_hash_hex.replace(/^0x/, '');

const fbTx = await fireblocks.createTransaction({
assetId: 'ETH',
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 }] } },
});

const result = await waitForTxCompletion(fbTx);
const signature = result.signedMessages[0].signature;

return { r: '0x' + signature.r, s: '0x' + signature.s, v: 27 + signature.v };
};

const { r, s, v } = await signWithFireblocks(crafted);

3. Prepare the signed transaction

Send the crafted serialized_tx_hex with the signature parts to get the final signed transaction.

const prepared = await api('/api/v1/monad/native/action/prepare', {
serialized_tx_hex: crafted.serialized_tx_hex,
r,
s,
v,
});
// 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/monad/native/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: Endpoints.
  • The same four-step flow on other networks and the conceptual overview: How it works.