Transaction building

Create and structure Stacks transactions for any use case

Transactions are the fundamental way to interact with the Stacks blockchain. This guide covers how to build, structure, and broadcast transactions using the Stacks.js transaction builder API.

Transaction anatomy

Every Stacks transaction shares a common structure with required components that define how it executes on the blockchain.

interface StacksTransaction {
version: TransactionVersion; // Mainnet or testnet
chainId: ChainID; // Network chain ID
auth: Authorization; // Signature(s)
anchorMode: AnchorMode; // Block anchoring strategy
postConditionMode: PostConditionMode; // Strict or allow
postConditions: PostCondition[]; // Security constraints
payload: TransactionPayload; // The actual operation
}

The payload determines the transaction type: STX transfer, contract deployment, or contract call. Each component plays a critical role in transaction validation and execution.

Building your first transaction

The simplest transaction type is an STX transfer. Here's how to create and broadcast one with proper error handling.

import {
makeSTXTokenTransfer,
broadcastTransaction,
AnchorMode,
PostConditionMode
} from '@stacks/transactions';
import { STACKS_TESTNET } from '@stacks/network';
async function sendSTX() {
const network = STACKS_TESTNET;
const txOptions = {
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 1000000n, // 1 STX = 1,000,000 microSTX
senderKey: 'your-private-key-here',
network,
memo: 'First transaction!',
fee: 200n, // Set fee or estimate dynamically
nonce: 0n, // Set manually or fetch from API
anchorMode: AnchorMode.Any,
postConditionMode: PostConditionMode.Deny
};
const transaction = await makeSTXTokenTransfer(txOptions);
// Broadcast to network
const broadcastResponse = await broadcastTransaction({ transaction });
if (broadcastResponse.error) {
throw new Error(`Broadcast failed: ${broadcastResponse.reason}`);
}
console.log('Transaction ID:', broadcastResponse.txid);
return broadcastResponse.txid;
}

Key transaction parameters

// Amount and fee use bigint for precision
amount: 1000000n // Always in microSTX (1 STX = 1,000,000 microSTX)
fee: 200n // Transaction fee in microSTX
nonce: 0n // Sequential counter for account transactions
// Network determines API endpoints and chain ID
network: STACKS_TESTNET // or STACKS_MAINNET
// Anchor mode affects confirmation time
anchorMode: AnchorMode.Any // Most flexible option

Transaction options

All transaction builder functions accept a common set of options that control execution behavior.

interface TransactionOptions {
network: StacksNetwork; // Target network configuration
anchorMode: AnchorMode; // Block anchoring strategy
fee?: bigint; // Transaction fee in microSTX
nonce?: bigint; // Account sequence number
postConditions?: PostCondition[]; // Asset transfer constraints
postConditionMode?: PostConditionMode; // Security mode
sponsored?: boolean; // Whether fee is paid by sponsor
}

Working with post-conditions

Post-conditions protect users by ensuring specific asset transfers occur. Use the Pc helper for type-safe construction.

import { Pc, PostConditionMode } from '@stacks/transactions';
// Define addresses for the example
const senderAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';
const contractAddress = 'ST000000000000000000002AMW42H';
const txOptions = {
// ... other options
postConditions: [
// Ensure sender sends exactly 1 STX
Pc.principal(senderAddress)
.willSendEq(1000000)
.ustx(),
// Ensure contract sends tokens to recipient
Pc.principal(contractAddress)
.willSendGte(100)
.ft(contractAddress + '.my-token', 'my-token')
],
postConditionMode: PostConditionMode.Deny // Strict mode
};

Anchor modes explained

enum AnchorMode {
OnChainOnly = 0x01, // Only in Bitcoin-anchored blocks (slower)
OffChainOnly = 0x02, // Only in microblocks (faster, less secure)
Any = 0x03, // Either type (recommended)
}

Use AnchorMode.Any unless you have specific security requirements. OnChainOnly provides maximum security but slower confirmation.

Fee estimation

Accurate fee estimation ensures transactions confirm quickly without overpaying. Here's how to build transactions with dynamic fee estimation.

import {
fetchFeeEstimateTransaction,
makeSTXTokenTransfer,
setFee,
broadcastTransaction
} from '@stacks/transactions';
async function buildTransactionWithEstimatedFee() {
// Step 1: Build transaction with minimal fee
const transaction = await makeSTXTokenTransfer({
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 1000000n,
senderKey: privateKey,
network,
fee: 1n, // Minimal fee for estimation
nonce: 0n,
anchorMode: AnchorMode.Any,
});
// Step 2: Estimate appropriate fee
const feeEstimate = await fetchFeeEstimateTransaction({
transaction,
network
});
// Step 3: Update transaction with estimated fee
setFee(transaction, feeEstimate.medium);
// Step 4: Broadcast with accurate fee
const result = await broadcastTransaction({ transaction });
return result.txid;
}

Building with fee rate preferences

async function buildWithFeePreference(priority: 'low' | 'medium' | 'high') {
// Build initial transaction
const tempTx = await makeSTXTokenTransfer({
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 5000000n, // 5 STX
senderKey: privateKey,
network,
fee: 1n,
nonce: await getCurrentNonce(senderAddress),
});
// Get fee estimates
const fees = await fetchFeeEstimateTransaction({
transaction: tempTx,
network
});
// Rebuild transaction with selected fee
const finalTx = await makeSTXTokenTransfer({
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 5000000n,
senderKey: privateKey,
network,
fee: fees[priority], // Use selected priority
nonce: await getCurrentNonce(senderAddress),
});
return broadcastTransaction({ transaction: finalTx });
}
// Usage
const txid = await buildWithFeePreference('high'); // Fast confirmation

Transaction serialization

Serialize transactions for storage, transmission, or offline signing workflows. This enables building transactions in one environment and signing/broadcasting in another.

import {
makeUnsignedSTXTokenTransfer,
deserializeTransaction,
TransactionSigner,
BytesReader
} from '@stacks/transactions';
import { bytesToHex, hexToBytes } from '@stacks/common';
import { getPublicKeyFromPrivate } from '@stacks/encryption';
// Complete offline signing workflow
async function buildForOfflineSigning() {
// Step 1: Create unsigned transaction with public key
const publicKey = getPublicKeyFromPrivate(privateKey);
const unsignedTx = await makeUnsignedSTXTokenTransfer({
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 2500000n, // 2.5 STX
network,
publicKey,
nonce: 10n,
fee: 180n,
});
// Step 2: Serialize for transport
const serialized = unsignedTx.serialize();
const txHex = bytesToHex(serialized);
return txHex; // Send to offline signer
}
// On the offline signing device
function signOffline(txHex: string, privateKey: string) {
// Step 1: Deserialize the transaction
const serialized = hexToBytes(txHex);
const bytesReader = new BytesReader(serialized);
const unsignedTx = deserializeTransaction(bytesReader);
// Step 2: Sign the transaction
const signer = new TransactionSigner(unsignedTx);
signer.signOrigin(privateKey);
// Step 3: Serialize signed transaction
const signedTx = signer.transaction;
const signedSerialized = signedTx.serialize();
return bytesToHex(signedSerialized);
}
// Back online: broadcast the signed transaction
async function broadcastSigned(signedTxHex: string) {
const serialized = hexToBytes(signedTxHex);
const bytesReader = new BytesReader(serialized);
const signedTx = deserializeTransaction(bytesReader);
const result = await broadcastTransaction({ transaction: signedTx });
if (result.error) {
throw new Error(`Broadcast failed: ${result.reason}`);
}
return result.txid;
}

Building multi-signature transactions

async function buildMultisigTransaction() {
// Build unsigned transaction for 2-of-3 multisig
const unsignedTx = await makeUnsignedSTXTokenTransfer({
recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
amount: 10000000n, // 10 STX
network,
numSignatures: 2, // Required signatures
publicKeys: [publicKey1, publicKey2, publicKey3],
nonce: 0n,
fee: 300n,
});
// First signer
const signer1 = new TransactionSigner(unsignedTx);
signer1.signOrigin(privateKey1);
// Serialize partially signed tx
const partialHex = bytesToHex(signer1.transaction.serialize());
// Second signer (potentially different device/time)
const partialTx = deserializeTransaction(
new BytesReader(hexToBytes(partialHex))
);
const signer2 = new TransactionSigner(partialTx);
signer2.appendOrigin(privateKey2);
// Now ready to broadcast
return broadcastTransaction({ transaction: signer2.transaction });
}

Nonce management

The nonce is a sequential counter that prevents transaction replay. Every transaction from an account must have a unique, incrementing nonce.

import { fetchNonce } from '@stacks/transactions';
const senderAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';
// Fetch current nonce from network
const nonce = await fetchNonce({
address: senderAddress,
network
});
// Build transaction with fetched nonce
const transaction = await makeSTXTokenTransfer({
recipient: 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP',
amount: 1500000n,
senderKey: privateKey,
network,
nonce,
fee: 180n,
});
const result = await broadcastTransaction({ transaction });

Further reading