Skip to main content

Integration example

This page walks through a complete Magma liquid staking flow with the Staking API (craft → sign → prepare → broadcast), including the signing step. The example uses the delegate action (staking MON and receiving gMON); the same pattern applies to undelegate and withdraw: only the endpoint and request body change.

Magma runs on Monad, so signing is EVM-based (the same r, s, v flow as Monad 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 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 Magma delegate transaction. The minimum stake is 100 MON.

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

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', // use the Fireblocks asset id configured for your Monad vault
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/magma/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/magma/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 undelegate, withdraw, and balance queries): Endpoints.
  • The same four-step flow on other networks and the conceptual overview: How it works.