Skip to main content

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

Test on testnet first

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.