Building Transactions

Create and customize all types of Stacks transactions with precise control over signing, sponsorship, and multi-signature scenarios


Building transactions programmatically gives you complete control over how your application interacts with the Stacks blockchain. You can create signed transactions for immediate broadcast, unsigned transactions for multi-signature workflows, sponsored transactions to pay fees for users, and complex multi-signature transactions.

This approach is essential for backends, automated systems, and advanced dApps that need precise transaction control.

Feature overview

Stacks.js provides transaction builders for every blockchain operation:

STX transfers: Send native tokens between accounts with memo support Contract deployment: Deploy Clarity smart contracts with version control Contract calls: Execute functions with proper argument validation and post-conditions Sponsored transactions: Pay transaction fees for other users Multi-signature transactions: Require multiple signatures for enhanced security Fee and nonce management: Automatic estimation or explicit control

Required packages

Install the core transaction building package:

npm install @stacks/transactions@latest

The @stacks/network package is optional in v7 - you can use string literals for networks.

Building signed transactions

Signed transactions are ready to broadcast immediately. The private key signs the transaction during creation.

STX token transfer

Send STX tokens between accounts:

import { makeSTXTokenTransfer, broadcastTransaction } from '@stacks/transactions';
const transaction = await makeSTXTokenTransfer({
recipient: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG',
amount: '1000000', // 1 STX in microSTX
senderKey: 'your-private-key-hex',
network: 'testnet',
memo: 'Payment for services',
// nonce and fee are auto-fetched/estimated if not provided
});
const result = await broadcastTransaction({
transaction,
network: 'testnet'
});
console.log('Transaction ID:', result.txid);

Smart contract deployment

Deploy Clarity contracts to the blockchain:

import { makeContractDeploy } from '@stacks/transactions';
import { readFileSync } from 'fs';
const contractCode = readFileSync('./my-contract.clar', 'utf8');
const transaction = await makeContractDeploy({
contractName: 'my-awesome-contract',
codeBody: contractCode,
senderKey: 'your-private-key-hex',
network: 'testnet',
clarityVersion: 3, // Defaults to latest if not specified
});
const result = await broadcastTransaction({
transaction,
network: 'testnet'
});

Smart contract function call

Execute contract functions with arguments and post-conditions:

import { makeContractCall, Cl } from '@stacks/transactions';
const transaction = await makeContractCall({
contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
contractName: 'hello-world',
functionName: 'set-value',
functionArgs: [
Cl.uint(42),
Cl.stringUtf8('Hello Stacks!')
],
senderKey: 'your-private-key-hex',
network: 'testnet',
validateWithAbi: true, // Validates against contract ABI
postConditions: [], // Add safety conditions
});

Building unsigned transactions

Unsigned transactions require a separate signing step. Use these for multi-signature workflows or when you want to inspect transactions before signing.

Unsigned STX transfer

import { makeUnsignedSTXTokenTransfer } from '@stacks/transactions';
import { privateKeyToPublicKey } from '@stacks/transactions';
const publicKey = privateKeyToPublicKey('your-private-key-hex');
const unsignedTx = await makeUnsignedSTXTokenTransfer({
recipient: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG',
amount: '1000000',
publicKey: publicKey,
network: 'testnet',
memo: 'Unsigned transfer',
});
// Sign separately
import { TransactionSigner } from '@stacks/transactions';
const signer = new TransactionSigner(unsignedTx);
signer.signOrigin('your-private-key-hex');
// Now ready to broadcast
const signedTx = signer.getTxInComplete();

Unsigned contract call

import { makeUnsignedContractCall } from '@stacks/transactions';
const unsignedTx = await makeUnsignedContractCall({
contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
contractName: 'my-contract',
functionName: 'my-function',
functionArgs: [Cl.uint(100)],
publicKey: publicKey,
network: 'testnet',
});
// Sign with TransactionSigner as shown above

Building sponsored transactions

Sponsored transactions let you pay fees for other users. The origin creates and signs the transaction, then the sponsor adds their signature and fee.

Create sponsored transaction (origin side)

import { makeContractCall, Cl } from '@stacks/transactions';
// Origin creates transaction with sponsored: true
const sponsoredTx = await makeContractCall({
contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
contractName: 'my-contract',
functionName: 'user-action',
functionArgs: [Cl.standardPrincipal('ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG')],
senderKey: 'origin-private-key',
network: 'testnet',
sponsored: true, // This makes it a sponsored transaction
});
// Serialize for the sponsor
const serializedTx = sponsoredTx.serialize();

Complete sponsored transaction (sponsor side)

import { sponsorTransaction, deserializeTransaction } from '@stacks/transactions';
// Sponsor receives the serialized transaction
const deserializedTx = deserializeTransaction(serializedTx);
// Sponsor adds fee and signature
const completedTx = await sponsorTransaction({
transaction: deserializedTx,
sponsorPrivateKey: 'sponsor-private-key',
fee: '2000', // Sponsor pays the fee
network: 'testnet',
});
// Sponsor broadcasts the completed transaction
const result = await broadcastTransaction({
transaction: completedTx,
network: 'testnet'
});

Building multi-signature transactions

Multi-sig transactions require multiple signatures. Create unsigned transactions and collect signatures from multiple parties.

Create multi-sig transaction

import {
makeUnsignedSTXTokenTransfer,
TransactionSigner,
privateKeyToPublicKey
} from '@stacks/transactions';
// Collect public keys from all participants
const privateKeys = [
'private-key-1',
'private-key-2',
'private-key-3'
];
const publicKeys = privateKeys.map(privateKeyToPublicKey);
// Create unsigned multi-sig transaction
const multiSigTx = await makeUnsignedSTXTokenTransfer({
recipient: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG',
amount: '5000000',
numSignatures: 2, // Require 2 of 3 signatures
publicKeys: publicKeys,
network: 'testnet',
});

Collect signatures

const signer = new TransactionSigner(multiSigTx);
// First participant signs
signer.signOrigin(privateKeys[0]);
// Second participant signs
signer.signOrigin(privateKeys[1]);
// Add remaining public key (unsigned participant)
signer.appendOrigin(publicKeys[2]);
// Get completed transaction
const signedMultiSigTx = signer.getTxInComplete();

Configuration options

Custom network endpoints

Connect to custom Stacks nodes:

const transaction = await makeSTXTokenTransfer({
recipient: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG',
amount: '1000000',
senderKey: 'your-private-key',
network: 'testnet',
client: { baseUrl: 'http://localhost:3999' } // Custom devnet
});

Explicit fee and nonce control

Override automatic fee estimation and nonce fetching:

const transaction = await makeContractCall({
contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
contractName: 'my-contract',
functionName: 'my-function',
functionArgs: [],
senderKey: 'your-private-key',
network: 'testnet',
nonce: '42', // Explicit nonce
fee: '5000', // Explicit fee in microSTX
anchorMode: 'any' // Transaction timing preference
});

Advanced post-conditions

Add safety constraints using the new v7 post-condition format:

const stxPostCondition = {
type: 'stx-postcondition',
address: 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B',
condition: 'gte',
amount: '1000000'
};
const ftPostCondition = {
type: 'ft-postcondition',
address: 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B',
condition: 'eq',
amount: '100',
asset: 'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.my-token::token'
};
const transaction = await makeContractCall({
// ... other options
postConditions: [stxPostCondition, ftPostCondition]
});

Fee estimation

Get fee estimates before building transactions:

import { fetchFeeEstimate } from '@stacks/transactions';
// Estimate for a specific transaction
const feeEstimate = await fetchFeeEstimate({
transaction,
network: 'testnet'
});
// Use the estimate
const newTransaction = await makeSTXTokenTransfer({
// ... other options
fee: feeEstimate.toString()
});

Working with Clarity values

Use the Cl helper for building type-safe Clarity arguments:

import { Cl } from '@stacks/transactions';
const functionArgs = [
Cl.uint(42), // unsigned integer
Cl.int(-10), // signed integer
Cl.bool(true), // boolean
Cl.stringUtf8('Hello World'), // UTF-8 string
Cl.stringAscii('ASCII only'), // ASCII string
Cl.buffer(new Uint8Array([1, 2, 3])), // buffer
Cl.principal('ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG'), // principal
Cl.contractPrincipal('ST123...', 'my-contract'), // contract principal
Cl.list([Cl.uint(1), Cl.uint(2)]), // list
Cl.tuple({ name: Cl.stringUtf8('Alice') }), // tuple
Cl.some(Cl.uint(42)), // optional (some)
Cl.none(), // optional (none)
Cl.ok(Cl.uint(100)), // response (ok)
Cl.error(Cl.uint(404)) // response (error)
];

Error handling

Handle broadcast errors properly with the consistent v7 response format:

try {
const result = await broadcastTransaction({
transaction,
network: 'testnet'
});
if (result.error) {
console.error('Transaction rejected:', result.reason);
console.error('Details:', result.reason_data);
} else {
console.log('Success:', result.txid);
}
} catch (error) {
console.error('Network or serialization error:', error);
}

Best practices

Transaction serialization

Transactions serialize to hex strings in v7 (not byte arrays):

const transaction = await makeSTXTokenTransfer({ /* options */ });
const serialized = transaction.serialize(); // hex string
console.log('Serialized tx:', serialized);
// For bytes (if needed for compatibility)
const bytes = transaction.serializeBytes(); // Uint8Array

Nonce management

For multiple transactions, manage nonces carefully:

import { fetchNonce } from '@stacks/transactions';
let currentNonce = await fetchNonce({
address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
network: 'testnet'
});
// Send multiple transactions with incremented nonces
for (const recipient of recipients) {
const transaction = await makeSTXTokenTransfer({
recipient,
amount: '1000000',
senderKey: privateKey,
network: 'testnet',
nonce: currentNonce.toString()
});
await broadcastTransaction({ transaction, network: 'testnet' });
currentNonce++;
}

ABI validation

Always validate contract calls against ABIs:

const transaction = await makeContractCall({
contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
contractName: 'my-contract',
functionName: 'my-function',
functionArgs: [Cl.uint(42)],
senderKey: privateKey,
network: 'testnet',
validateWithAbi: true // Fetches ABI and validates arguments
});

This comprehensive approach to transaction building gives you the flexibility to handle any blockchain interaction scenario while maintaining type safety and proper error handling.