Integration example
This page walks through a complete Cosmos Hub staking flow with the Staking API (craft → sign → prepare → broadcast), including the signing step. The example uses the stake (delegate) action; the same pattern applies to unstake and claim-rewards: 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).
Set X-NETWORK to the chain you want to target (for example cosmoshub-4). X-NETWORK accepts the same identifier as the chain_id in the Cosmos Chain Registry. 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 @cosmjs/proto-signing @cosmjs/stargate @cosmjs/amino @cosmjs/encoding cosmjs-types dotenv
Fill in your values in .env. Provide either a mnemonic (COSMOS_WALLET_SEED) or a raw private key (COSMOS_WALLET_PRIVATE_KEY):
STAKELY_API_KEY=
STAKELY_API_BASE_URL=https://staking-api.stakely.io
STAKELY_NETWORK=cosmoshub-4
COSMOS_WALLET_PUBKEY=
COSMOS_WALLET_ADDRESS=
COSMOS_WALLET_SEED=
COSMOS_WALLET_PRIVATE_KEY=
COSMOS_RPC=
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. The pubkey is the wallet's secp256k1 public key (hex).
const crafted = await api('/api/v1/cosmoshub/native/action/stake', {
pubkey: process.env.COSMOS_WALLET_PUBKEY,
amount: 0.1,
});
// crafted = { unsigned_tx_hex, tx_auth_info_hex, unsigned_tx_hash_hex }
2. Sign the transaction
Cosmos prepare needs three values: tx_body_hex, tx_auth_info_hex (from the crafted response), and signature_hex. Use one of the two options below.
Option A: Local private key (Cosmos SDK JS)
Signs with a mnemonic or raw private key and returns both the signature_hex and the tx_body_hex.
const { fromHex, toHex } = require('@cosmjs/encoding');
const { TxRaw } = require('cosmjs-types/cosmos/tx/v1beta1/tx');
const { DirectSecp256k1HdWallet, DirectSecp256k1Wallet, makeSignDoc } = require('@cosmjs/proto-signing');
const { decodeSignature } = require('@cosmjs/amino');
const { SigningStargateClient } = require('@cosmjs/stargate');
// The bech32 prefix must match the target chain (e.g. "cosmos" for Cosmos Hub)
const prefix = 'cosmos';
const signWithPrivateKey = async ({ unsigned_tx_hex, tx_auth_info_hex }) => {
const unsignedTx = TxRaw.decode(fromHex(unsigned_tx_hex));
const authInfoBytes = fromHex(tx_auth_info_hex);
const wallet = process.env.COSMOS_WALLET_SEED
? await DirectSecp256k1HdWallet.fromMnemonic(process.env.COSMOS_WALLET_SEED, { prefix })
: await DirectSecp256k1Wallet.fromKey(fromHex(process.env.COSMOS_WALLET_PRIVATE_KEY), prefix);
const client = await SigningStargateClient.connectWithSigner(process.env.COSMOS_RPC, wallet);
const [account] = await wallet.getAccounts();
const { accountNumber } = await client.getSequence(account.address);
const chainId = await client.getChainId();
const signDoc = makeSignDoc(unsignedTx.bodyBytes, authInfoBytes, chainId, accountNumber);
const { signed, signature } = await wallet.signDirect(account.address, signDoc);
return {
signature_hex: toHex(decodeSignature(signature).signature),
tx_body_hex: toHex(signed.bodyBytes),
};
};
const { signature_hex, tx_body_hex } = await signWithPrivateKey(crafted);
Option B: External custodian (Fireblocks)
Fireblocks signs the unsigned_tx_hash_hex. The tx_body_hex that prepare needs is simply the body of the crafted transaction, so we derive it from unsigned_tx_hex.
npm install fireblocks-sdk
FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=
const { fromHex, toHex } = require('@cosmjs/encoding');
const { TxRaw } = require('cosmjs-types/cosmos/tx/v1beta1/tx');
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_hex, unsigned_tx_hash_hex }) => {
const fbTx = await fireblocks.createTransaction({
assetId: 'ATOM',
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: unsigned_tx_hash_hex }] } },
});
const result = await waitForTxCompletion(fbTx);
return {
signature_hex: result.signedMessages[0].signature.fullSig,
tx_body_hex: toHex(TxRaw.decode(fromHex(unsigned_tx_hex)).bodyBytes),
};
};
const { signature_hex, tx_body_hex } = await signWithFireblocks(crafted);
3. Prepare the signed transaction
Send tx_body_hex, the crafted tx_auth_info_hex, and signature_hex to assemble the signed transaction.
const prepared = await api('/api/v1/cosmoshub/native/action/prepare', {
tx_body_hex,
tx_auth_info_hex: crafted.tx_auth_info_hex,
signature_hex,
});
// 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/cosmoshub/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 (including
unstake,claim-rewards, and balance queries): Endpoints. - The same four-step flow on other networks and the conceptual overview: How it works.