Writing
Complete Guide: Monitoring Transactions on STX Addresses Using Stacks Blockchain
2025
Monitoring transactions on the Stacks blockchain is essential if you're building wallets, notification systems, or on-chain automations. This guide covers two real-time approaches to tracking STX address activity:
-
Using the mempool for instant detection of unconfirmed transactions.
-
Using Chainhooks for tracking confirmed transactions reliably.
🧠 What Are Chainhooks?
Chainhook is a reorg-aware event indexer for the Bitcoin and Stacks blockchains. It lets you build real-time, programmable event listeners that survive chain reorganizations, making it a reliable choice for critical workflows.
Why Use Chainhooks?
-
Real-time confirmed transaction monitoring
-
Reorg safety
-
Custom HTTP webhooks
What Is the Stacks API?
The Stacks API allows developers to query blockchain data, monitor addresses, or interact with smart contracts.
Hiro Platform
Hiro provides APIs, SDKs, monitoring tools, and infrastructure to simplify building on Bitcoin via Stacks.
🛠 Prerequisites
Before you begin, ensure you have the following: This article is not for complete beginners
-
Basic understanding of TypeScript, Nodejs and JavaScript.
-
Familiarity with blockchain concepts, particularly Stacks blockchain.
-
Node.js and npm installed on your development machine.
-
Access to a Stacks wallet (e.g., Leather or Xverse) for testing.
🚀 Project Setup
Start with a fresh Node.js + TypeScript project:
mkdir stx-transaction-monitor # creates a new folder for the project
cd stx-transaction-monitor # change our current directory to the project's
npm init -y # init a new node application
npm install --save @stacks/blockchain-api-client hono axios uuid # install packages used
npm install --save-dev @types/node @types/uuid typescript tsx nodemon # install dev dependencies
npx tsc --init
package.json Scripts
{
"scripts": {
"dev": "nodemon --exec tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
🗂 Folder Structure
stx-transaction-monitor/
├── src/
│ ├── lib/
│ │ └── constants.ts
│ ├── services/
│ │ ├── chainhooks.ts
│ │ ├── stacks-socket.ts
│ │ └── getStacksInfo.ts
│ └── index.ts
├── .env
├── .gitignore
├── package.json
├── tsconfig.json
⚙️ Environment Variables
Create .env file:
HIRO_API_KEY=your_hiro_platform_api_key_here
AUTHORIZATION_HEADER=Bearer your_auth_token_here
API_BASE_URL=https://your-ngrok-url.ngrok.io
NODE_ENV=development
Create .gitignore: This file is going to tell github which files/folders to ignore
We don’t want our node_modules on github 😅
node_modules/
dist/
.env
*.log
🔧 Config File: constants.ts
Central config file for API endpoints and env variables.
export const HIRO_PLATFORM_API_BASE_URL = "https://api.platform.hiro.so/"; // Api base url from hiro
export const MAINNET_API_URL = "https://api.mainnet.hiro.so"; // stacks api mainnet url
export const TESTNET_API_URL = "https://api.testnet.hiro.so"; // stacks api testnet ur;
export const AUTHORIZATION_HEADER = process.env.AUTHORIZATION_HEADER!; // auth header to verify chainhook is from our app
export const HIRO_PLATFORM_API_KEY = process.env.HIRO_API_KEY!; // Hiro api key gotten from https://platform.hiro.so/
export const API_BASE_URL = process.env.API_BASE_URL!; // temp api url
// select which api to use based on the environment
export const STACKS_API_URL = process.env.NODE_ENV === 'production'
? MAINNET_API_URL
: TESTNET_API_URL;
🔌 stacks-socket.ts
Sets up a WebSocket client to subscribe to mempool and address transactions.
import { StacksApiSocketClient } from '@stacks/blockchain-api-client';
import { STACKS_API_URL } from '../lib/constants.js';
// Create stacks websocket client to listen for events
export const sc = new StacksApiSocketClient({ url: STACKS_API_URL });
🔌 getStacksInfo.ts
Fetches latest blockchain tip info required for Chainhook creation.
import axios from 'axios';
export const getStacksInfo = async () => {
const url = "https://stacks-node-api.mainnet.stacks.co/v2/info";
const { data } = await axios.get(url);
return data;
};
🔌 chainhooks.ts
Handles creation and deletion of chainhooks via Hiro API.
import axios from "axios";
import { v4 as uuidV4 } from "uuid";
import { getStacksInfo } from "./getStacksInfo.js";
import { API_BASE_URL, AUTHORIZATION_HEADER, HIRO_PLATFORM_API_BASE_URL, HIRO_PLATFORM_API_KEY } from "../lib/constants.js";
export const createChainhook = async (txID, walletAddr, memo) => {
// Get stacks info
const stacksInfo = await getStacksInfo();
// Generate a new id for the chainhook
const uuid = uuidV4();
// api url to create a new chainhook
const url = `${HIRO_PLATFORM_API_BASE_URL}v1/ext/${HIRO_PLATFORM_API_KEY}/chainhooks`;
const chainhookPayload = {
name: `${memo}|${walletAddr}|${txID}`,
uuid,
chain: "stacks",
version: 1,
networks: {
mainnet: {
if_this: { scope: "txid", equals: txID },
// Use tip_height from stacks info to let the chainhook know where it should start tracking
start_block: stacksInfo.stacks_tip_height,
then_that: {
http_post: {
url: `${API_BASE_URL}/api/track`,
authorization_header: AUTHORIZATION_HEADER
}
},
decode_clarity_values: true,
expire_after_occurrence: 1
}
}
};
const { data } = await axios.post(url, chainhookPayload, {
headers: {
'Content-Type': 'application/json',
// IMPORTANT: use the hiro platform api key
'Authorization': `Bearer ${HIRO_PLATFORM_API_KEY}`
}
});
return data;
};
🧠 Main Logic: index.ts
This file wires everything together.
Setup
import { Hono } from "hono";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { sc } from "./services/stacks-socket.js";
import { createChainhook } from "./services/chainhooks.js";
const app = new Hono();
app.use(logger());
app.use(prettyJSON());
const addressesToTrack = ["SP3...", "SP1..."];
Method 1: Watch Mempool
Every transaction hits the mempool before being confirmed. We listen to this waiting area. If any tracked address sends or receives a transaction, we detect it instantly and trigger an action (like logging or alerting). The limitation of this is that it doesn’t guarantee if the transaction was successful or failed. We would handle this in out next method.
sc.subscribeMempool((tx) => {
if (addressesToTrack.includes(tx.sender_address) || addressesToTrack.includes(tx.recipient_address)) {
// One of the addreses we track have a new transaction, handle it here e.g
// sendTelegramNotification() ....
}
});
Method 2: Watch Mempool + Chainhook
Almost as the same as the first method right? but here you can see we create a new chainhook, this way we can handle when the status of the transaction changes via a Webhook we are going to setup. Stay with me now
sc.subscribeMempool((tx) => {
if (addressesToTrack.includes(tx.sender_address) || addressesToTrack.includes(tx.recipient_address)) {
createChainhook(tx.tx_id, tx.sender_address, "mempool-triggered").catch(console.error);
}
});
Setting up our webhook
Here we create a new api route using Hono on /api/track
app.post("/api/track", async (c) => {
try {
const body = await c.req.json();
console.log("🔔 Chainhook notification received:");
// Inspect the data its usually contains information about the transaction
console.log(JSON.stringify(body, null, 2));
return c.json({
message: "Webhook received successfully",
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("❌ Error processing webhook:", error);
return c.json({ error: "Failed to process webhook" }, 500);
}
});
With this method 2 is complete.
Method 3: Subscribe to single Address for confirmed transactions
const singleAddressToTrack = "SP3C5SSYVKPAWTR8Y63CVYBR65GD3MG7K80526D1Q";
sc.subscribeAddressTransactions(singleAddressToTrack, (address, tx) => {
// Confirmed transactions for the address would be available here
console.log(`✅ Confirmed transaction for ${address}`);
console.log(`TX ID: ${tx.tx_id}`);
console.log(`Block: ${tx.block_height}`);
console.log(`Status: ${tx.tx_status}`);
// Process confirmed transaction here
});
Start the server
// Start the server
const port = process.env.PORT || 3000;
console.log(`🚀 Starting STX transaction monitor on port ${port}`);
console.log(`📡 Tracking ${addressesToTrack.length} addresses`);
console.log(`🔗 WebSocket connecting to Stacks API...`);
export default {
port,
fetch: app.fetch,
};
🧪 Testing
Run server:
npm run dev
Expose local server:
ngrok http 3000
Update .env with the ngrok URL.
Check endpoints:
curl http://localhost:3000/health
curl -X POST http://localhost:3000/api/track -H "Content-Type: application/json" -d '{"test": true}'
Now you may be asking, “Atomic Sensei, why not just loop through our list of wallets and call sc.subscribeAddressTransactions for each one?”
Calm down, gakusei. That method works, but only for one address at a time. Looping through many creates too many WebSocket connections, which is inefficient and can cause issues. Use it only when you’re tracking one important wallet. For multiple wallets, stick to the mempool or mempool+chainhook for confirmed transactions.
That would be all for now.
✅ Summary
This project provides:
-
Mempool monitoring for near-instant detection
-
Confirmed tracking via chainhooks
-
Scalable real-time event handling for wallets, bots, or analytics
Benefits
-
Fast: mempool catches events immediately
-
Reliable: chainhooks confirm finality
-
Scalable: monitor many addresses
-
Extendable: supports NFTs, contract calls, custom logic
🚨 Next Steps
-
Replace addresses with real data
-
Implement your webhook logic
-
Add DB or queue system
-
Harden production reliability with retries, logging, rate limits