Tracking UTXOs for a Specific Address
Introduction
Welcome to your first hands-on tutorial with Yaci Store! In this guide, you'll learn how to set up a lightweight Cardano indexer that tracks transactions and UTXOs (Unspent Transaction Outputs) for a specific address.
What is Yaci Store? Yaci Store is a Cardano blockchain indexer that reads data from the blockchain and stores it in a PostgreSQL database. Think of it as a specialized tool that helps you extract and organize blockchain data for your applications.
What will you build? By the end of this tutorial, you'll have a running Yaci Store instance that:
- Synchronizes with the Cardano blockchain
- Tracks UTXOs only for one specific address (ignoring everything else)
- Stores the data in an easily queryable database
- Optionally sends you Discord notifications when the address balance changes
Why is this useful? Instead of indexing the entire Cardano blockchain (which requires significant storage and processing power), you can focus on just the data you need. This is perfect for:
- Monitoring a treasury wallet
- Tracking payments to a specific address
- Building lightweight applications that only care about one address
- Development and testing with minimal resource requirements
What you'll learn:
- How to set up Yaci Store using Docker
- How to configure and enable plugins
- How to write custom filtering logic
- How to query blockchain data from the database
Let's get started!
Prerequisites
Before we begin, make sure you have:
- Docker and Docker Compose installed on your system
- Install Docker Desktop (opens in a new tab) for Mac or Windows
- Install Docker Engine (opens in a new tab) for Linux
- Basic familiarity with Cardano addresses
- Estimated time: 20-30 minutes
This tutorial uses the Cardano preprod testnet, which is perfect for learning without dealing with real ADA or requiring extensive storage.
Step 1: Download Yaci Store
First, let's get the Yaci Store Docker distribution.
-
Download the latest release
Visit the Yaci Store releases page (opens in a new tab) and download the Docker distribution archive (look for
yaci-store-docker-<version>.zip).For example:
yaci-store-docker-2.0.0-beta5.zip -
Extract the archive
# For .tar.gz files unzip yaci-store-docker-<version>.zip cd yaci-store-docker-<version> -
Explore the directory structure
You should see the following files and folders:
yaci-store/ ├── yaci-store.sh # Main script to start/stop Yaci Store ├── admin-cli.sh # Admin operations ├── monitoring.sh # Optional monitoring stack ├── psql.sh # Quick database access ├── LICENSE # License ├── compose/ # Docker compose files │ ├── .env # Docker image versions │ ├── admin-cli-compose.yml │ ├── monitoring.yml │ ├── postgres-compose.yml │ ├── postgres-compose-minimal.yml │ ├── yaci-store.yml │ └── yaci-store-monolith.yml ├── config/ # Configuration files │ ├── env # Environment variables │ ├── application.properties │ ├── application-ledger-state.properties │ └── application-plugins.yml └── plugins/ # Where we'll put our custom plugins ├── ext-jars └── scripts
The yaci-store.sh script is your main interface. It handles starting, stopping, and managing the Docker containers for you.
Step 2: Configure Network Connection
Now let's configure Yaci Store to connect to the Cardano preprod testnet.
-
Edit the main configuration file
Open
config/application.propertiesin your text editor. -
Set the network connection
Find the Cardano network configuration section and set these values:
# Cardano Network Configuration store.cardano.host=preprod-node.play.dev.cardano.org store.cardano.port=3001 store.cardano.protocol-magic=1What do these settings mean?
host: The Cardano node you'll connect to (we're using a public preprod node)port: The port number for the node connectionprotocol-magic: Network identifier (1 = preprod, 2 = preview, 764824073 = mainnet)
-
Verify database settings
The Docker distribution comes pre-configured for database access. You should see:
spring.datasource.url=jdbc:postgresql://yaci-store-postgres:5432/yaci_store?currentSchema=yaci_store spring.datasource.username=yaci spring.datasource.password=dbpass⚠️These are development credentials. For production use, change the database password!
Step 3: Optimize for Address Tracking
Since we only want to track one address, let's disable stores we don't need. This dramatically reduces resource usage.
-
Continue editing
config/application.properties -
Disable unnecessary stores
Add or modify these settings:
# Disable stores we don't need store.assets.enabled=false store.blocks.enabled=false store.epoch.enabled=false store.metadata.enabled=false store.mir.enabled=false store.script.enabled=false store.staking.enabled=false store.transaction.enabled=false store.governance.enabled=false # Keep the UTXO store enabled (we need this!) store.utxo.enabled=true -
Set a recent starting point (optional but recommended)
Instead of syncing from the beginning of the blockchain, we can start from a recent point. However, to get accurate balance or UTXOs for your selected address, you need to choose a point before the first transaction of that address. If it's a new address with no transactions, you can select the current tip.
The following configuration is based on the example address used in this tutorial. Change these values according to your selected address.
# Start syncing from a recent slot (adjust to current preprod slot) store.cardano.sync-start-slot=107754724 store.cardano.sync-start-blockhash=534d3c2d1c3915523ea8843449dd91fcf4d647719d9ef2dc64a97d47be39447bTo get the latest preprod slot and block hash, visit Cardano Explorer (Preprod) (opens in a new tab) or use a recent block. This allows you to start syncing quickly without processing historical data.
-
Save the file
Step 4: Enable the Plugin System
Yaci Store uses a plugin system to extend its functionality. Let's enable it.
4.1 Enable Plugin Loading
-
Edit
config/envFind and uncomment (remove the
#at the beginning) this line:JDK_JAVA_OPTIONS=${JDK_JAVA_OPTIONS} -Dloader.path=plugins,plugins/lib,plugins/ext-jarsThis tells Yaci Store to load plugins from the
pluginsdirectory.
4.2 Enable Plugins in Configuration
-
Edit
config/application-plugins.yml -
Enable the plugin system
Make sure this is set:
store: plugins: enabled: true
Step 5: Create the Plugin Scripts
Now comes the fun part - writing the JavaScript plugins that will filter UTXOs by address!
We'll create three plugin files:
5.1 Create the Address Filter
-
Create a new file
plugins/scripts/filter-by-address.js -
Add this code:
/** * Filters a list of UTXO items by a specific address * @param {Array} items - List of UTXO items to filter * @return {Array} List of filtered UTXOs that match the address filter */ function filterByAddress(items) { // Get the target address from configuration // Get the address filter from environment configuration const address = env.getProperty("address.filter"); // Filter UTXOs by owner address using functional approach const filteredUtxos = items.filter(item => item.getOwnerAddr() === address); // If any UTXOs were found, update global state and log the count if (filteredUtxos.length > 0) { global_state.put("utxo.found", true); //console.log("Utxo found : " + filteredUtxos.length); } return filteredUtxos; }What does this do?
- Gets the target address from your configuration
- Filters incoming UTXOs to only keep ones for that address
- Returns the filtered list (which is what gets saved to the database)
5.2 Create the Commit Event Handler
-
Create a new file
plugins/scripts/handle-commit.js -
Add this code:
/** * Handles commit events by cleaning up additional transaction inputs * * APPROACH: Since tx_input records don't have address filtering and Yaci Store processes * blocks in parallel batches (100 blocks during initial sync, 1 block at tip), tx_input * records may be inserted before their corresponding UTXO entries in address_utxo table. * The CommitEvent is published at the end of each batch and handlers are processed * sequentially, making it the perfect place to delete orphaned tx_input records. * * @param {Object} event - The commit event containing metadata like slot number */ function handleCommitEvent(event) { // Get the last processed slot from global_state, default to 0 if not set let last_tx_inputs_slot = global_state.get("last_tx_inputs_slot"); if (last_tx_inputs_slot === null || last_tx_inputs_slot === undefined) { last_tx_inputs_slot = 0; } console.log("Deleting additional tx inputs after slot: " + last_tx_inputs_slot); // SQL query to delete orphaned tx_input records // Only deletes records that are not referenced by address_utxo table const sql = "DELETE FROM tx_input ti " + "WHERE ti.spent_at_slot > :given_slot " + "AND NOT EXISTS ( " + " SELECT 1 FROM address_utxo au " + " WHERE au.tx_hash = ti.tx_hash " + " AND au.output_index = ti.output_index " + ")"; // Set parameters for the SQL query const params = { "given_slot": last_tx_inputs_slot }; // Execute the delete operation and get count of deleted records const count = named_jdbc.update(sql, params); // Log the number of deleted records if any were found if (count > 0) { console.log("Deleted " + count + " additional tx inputs."); } // Update the last processed slot to current event slot global_state.put("last_tx_inputs_slot", event.getMetadata().getSlot()); }What does this do?
- Deletes any transaction inputs that don't belong to our target address
5.3 Create the Rollback event Handler
-
Create a new file
plugins/scripts/handle-rollback.js -
Add this code:
function handleRollbackEvent(event) { //Reset last_tx_input_slot used in CommitEvent to delete additional tx inputs console.log("Utxo by address plugin handleRollbackEvent: " + event.getRollbackTo().getSlot()); global_state.put("last_tx_inputs_slot", event.getRollbackTo().getSlot()); }
5.4 Create the Discord Notification (Optional)
This plugin sends you a Discord message when your address balance changes!
-
Create a new file
plugins/scripts/notify-discord.js -
Add this code:
/** * Sends Discord notification when balance changes for the monitored address * @param {Object} event - The commit event containing metadata like block number */ function sendDiscordNotificationOnBalanceChange(event) { const utxoFound = global_state.get("utxo.found"); if (utxoFound !== null && utxoFound) { global_state.remove("utxo.found"); // Skip sending notification if not at tip yet if (!event.getMetadata().isSyncMode()) { return; } const address = env.getProperty("address.filter"); const sql = "SELECT SUM(au.lovelace_amount) AS balance " + "FROM address_utxo au " + "WHERE au.owner_addr = :address " + "AND NOT EXISTS (SELECT 1 FROM tx_input ti WHERE ti.tx_hash = au.tx_hash AND ti.output_index = au.output_index)"; const params = { "address": address }; const result = named_jdbc.queryForMap(sql, params); const balance = result["balance"]; const discordUrl = env.getProperty("discord.webhook.url"); const jsonData = { "content": `💰 Unspent UTXO Balance: ${balance} Lovelace at Block: ${event.getMetadata().getBlock()}\n Address: ${address}` }; const response = http.postJson(discordUrl, jsonData, { "Content-Type": "application/json" }); console.log("Discord response: " + response); } }What does this do?
- Calculates the current balance of your address from the database
- Formats a nice Discord embed message
- Sends it to your Discord webhook whenever a UTXO is added
Step 6: Configure the Plugins
Now let's wire up these plugins in the configuration.
-
Edit
config/application-plugins.yml -
Replace the contents with this configuration:
store:
plugins:
enabled: true
api-enabled: false
metrics:
enabled: false
# Filter plugins run BEFORE data is saved
filters:
utxo.unspent.save:
- name: "Filter UTXOs by Address"
lang: js
script:
file: /app/plugins/scripts/filter-by-address.js
function: filterByAddress
# Event handlers react to blockchain events
event-handlers:
CommitEvent:
- name: "Clean Orphaned TxInputs"
lang: js
script:
file: /app/plugins/scripts/handle-commit.js
function: handleCommitEvent
- name: "Send Balance Change Notification"
lang: js
script:
file: /app/plugins/scripts/notify-discord.js
function: sendDiscordNotificationOnBalanceChange
RollbackEvent:
- name: "Handle Rollback Event"
lang: js
script:
file: /app/plugins/scripts/handle-rollback.js
function: handleRollbackEventNote that the local plugins folder is mapped to /app/plugins for the Yaci Store docker distribution.
-
Set your target address in
config/application.propertiesAdd this line (replace with the address you want to track):
# The Cardano address to track address.filter=addr_test1qp5z7zmlw848szvmpe693ukfu9yq4t3we4s6u483j9sq4zr09qy27awlm22wdh98zm2le5l8yluezf5amygq5d6htj5qdx4p3kThis is a preprod testnet address. Replace it with any preprod address you want to track! You can get test ADA from the Cardano Testnet Faucet (opens in a new tab).
-
(Optional) Set up Discord webhook
If you want balance notifications, add this to
config/application.properties:# Optional: Discord webhook URL for notifications discord.webhook.url=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKENTo create a Discord webhook:
- Go to your Discord server settings
- Navigate to Integrations → Webhooks
- Create a new webhook and copy the URL
Step 7: Start Yaci Store
Time to launch everything!
-
Start Yaci Store
./yaci-store.sh startThis will:
- Pull the necessary Docker images (first time only)
- Start PostgreSQL database
- Start Yaci Store application
- Begin synchronizing with the blockchain
-
Watch the logs
./yaci-store.sh logs:yaci-store
Step 8: Query Your Data
Let's verify that everything is working by querying the database!
-
Connect to the database
./psql.shThis opens a PostgreSQL console connected to your Yaci Store database.
-
Set the schema
SET search_path TO yaci_store; -
Check UTXOs for the monitored address
SELECT owner_addr, lovelace_amount as lovelace, amounts, tx_hash, output_index, slot FROM address_utxo WHERE owner_addr = <monitored_address> ORDER BY slot DESC LIMIT 10;What you'll see:
- All UTXOs currently available at your tracked address
- The amounts in each UTXO
- The transaction hash and output index
- The slot number when it was created
-
Calculate address balance
SELECT SUM(au.lovelace_amount) AS balance FROM address_utxo au WHERE au.owner_addr = <monitored_address> AND NOT EXISTS (SELECT 1 FROM tx_input ti WHERE ti.tx_hash = au.tx_hash AND ti.output_index = au.output_index); -
Exit the database console
\q
If you don't see any data yet, the address might not have received any transactions since your sync starting point. Try sending some test ADA to your tracked address from the testnet faucet (opens in a new tab)!
Step 9: Understanding Your Setup
What's happening behind the scenes?
-
Yaci Store connects to the Cardano node and requests blockchain data starting from your configured slot
-
The node streams blocks containing all transactions and UTXOs
-
Your filter plugin runs for every UTXO before it's saved:
- If the UTXO belongs to your address → it gets saved
- If it belongs to any other address → it's discarded
-
The commit event handler cleans up any transaction input records that don't match your address
-
The notification plugin (if configured) sends you a Discord message whenever new UTXOs arrive
Resource usage
Because you're only tracking one address:
- Database size: Very small (megabytes instead of gigabytes)
- CPU usage: Minimal
- Memory: ~1-2GB for the Docker containers
- Sync time: Fast (only processes blocks with relevant transactions)
Customizing for your needs
Want to track a different address? Just:
- Stop Yaci Store:
./yaci-store.sh stop - Change
address.filterinconfig/application.properties - Update
store.cardano.sync-start-slotandstore.cardano.sync-start-blockhashinconfig/application.properties(If required) - Reset the database:
sudo rm -rf db-data - Start again:
./yaci-store.sh start
Troubleshooting
Plugins not loading?
Check that you:
- Uncommented the plugin loader line in
config/env - Set
store.plugins.enabled=trueinconfig/application-plugins.yml - Used the correct file paths (they should start with
/app/plugins/scripts/)
No data appearing?
Verify:
- Your
address.filteris set correctly - The address has received transactions after your sync starting slot
- Yaci Store is connected to the network (check logs)
Discord notifications not working?
- Make sure your webhook URL is correct
- Check that the address has actually received new UTXOs
- Look at the logs for any error messages
Need to restart?
./yaci-store.sh stop
./yaci-store.sh startNext Steps
Congratulations! You've successfully set up a Yaci Store instance with custom plugins.
Learn more
- Plugin Framework Documentation - Dive deeper into plugin development
- Plugin API Reference - Available context variables and functions
- Database Access - Advanced database queries in plugins
- HTTP Client - Make HTTP requests from plugins
Explore more plugins
Check out the yaci-store-plugins repository (opens in a new tab) for more examples.
Try these modifications
- Track multiple addresses: Modify the filter to accept a list of addresses
- Add native token tracking: Extend the queries to show native tokens (not just ADA)
- Add webhook notifications: Send HTTP callbacks instead of Discord messages
Summary
In this tutorial, you learned:
- ✅ How to download and set up Yaci Store with Docker
- ✅ How to configure network connections and optimize settings
- ✅ How to enable and write custom JavaScript plugins
- ✅ How to filter blockchain data for specific addresses
- ✅ How to query blockchain data from PostgreSQL
- ✅ How to set up Discord notifications (optional)
You now have a powerful, lightweight Cardano indexer that tracks exactly the data you need. This is just the beginning - the plugin system opens up endless possibilities for customizing Yaci Store to your specific use case!
Happy indexing! 🚀