Querying UTxOs
Query unspent transaction outputs for addresses and transaction references
UTxOs (Unspent Transaction Outputs) are the fundamental building blocks of Cardano transactions. This guide shows you how to query UTxOs efficiently.
Choose Your Network
| Network | Endpoint |
|---|---|
| Mainnet | https://api.nacho.builders/v1/ogmios |
| Preprod | https://api.nacho.builders/v1/preprod/ogmios |
Your API key works on both networks. All examples below use Mainnet - add /preprod to the path for testnet.
Query by Address
The most common query - get all UTxOs at a specific address:
const response = await fetch('https://api.nacho.builders/v1/ogmios', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.NACHO_API_KEY
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'queryLedgerState/utxo',
params: {
addresses: [
'addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'
]
}
})
});
const { result } = await response.json();
// result is an array of UTxOs
result.forEach(utxo => {
console.log('TxId:', utxo.transaction.id);
console.log('Index:', utxo.index);
console.log('Value:', utxo.value);
});Query Multiple Addresses
Query UTxOs for multiple addresses in a single request:
const response = await fetch('https://api.nacho.builders/v1/ogmios', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.NACHO_API_KEY
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'queryLedgerState/utxo',
params: {
addresses: [
'addr1...', // Address 1
'addr1...', // Address 2
'addr1...' // Address 3
]
}
})
});Batch multiple addresses into a single request to reduce API calls and improve performance.
Query by Transaction Reference
Look up a specific UTxO by its transaction ID and output index:
const response = await fetch('https://api.nacho.builders/v1/ogmios', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.NACHO_API_KEY
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'queryLedgerState/utxo',
params: {
outputReferences: [
{
transaction: { id: '3e40d...abcdef' },
index: 0
}
]
}
})
});
const { result } = await response.json();
// Returns the specific UTxO if it exists and is unspentUTxO Response Structure
Each UTxO in the response contains:
{
"transaction": {
"id": "3e40d...abcdef"
},
"index": 0,
"address": "addr1...",
"value": {
"ada": {
"lovelace": 5000000
},
"policyId123...": {
"tokenName": 100
}
},
"datum": "d8799f...", // Inline datum (if present)
"datumHash": "abc123...", // Datum hash (if present)
"script": { ... } // Reference script (if present)
}| Field | Description |
|---|---|
transaction.id | The transaction hash that created this UTxO |
index | Output index within the transaction |
address | The address holding this UTxO |
value.ada.lovelace | ADA amount in lovelace (1 ADA = 1,000,000 lovelace) |
value.[policyId] | Native tokens by policy ID and asset name |
datum | Inline datum in CBOR hex (Plutus V2+) |
datumHash | Hash of datum (Plutus V1 style) |
script | Reference script attached to UTxO |
Working with Native Tokens
Parse and display native tokens from UTxOs:
/**
* Extract all native tokens from a UTxO.
* Handles the nested value structure where tokens are grouped by policy ID.
*
* @param utxo - UTxO object with value field
* @returns Array of token objects with metadata
*/
function parseTokens(utxo) {
const tokens = [];
// Iterate through all entries in the value object
for (const [policyId, assets] of Object.entries(utxo.value)) {
// Skip ADA (it's stored separately from native tokens)
if (policyId === 'ada') continue;
// Each policy ID can have multiple assets (different token names)
for (const [assetName, quantity] of Object.entries(assets)) {
tokens.push({
policyId, // 28-byte hex hash of minting policy
assetName, // Hex-encoded asset name
assetNameHex: assetName,
// Decode hex to human-readable name (may be binary/non-UTF8)
assetNameUtf8: Buffer.from(assetName, 'hex').toString('utf8'),
quantity // Token amount (integer)
});
}
}
return tokens;
}
// Usage example: list all tokens held by an address
const utxos = await getAddressUtxos('addr1...');
utxos.forEach(utxo => {
const tokens = parseTokens(utxo);
tokens.forEach(t => {
console.log(`${t.assetNameUtf8}: ${t.quantity}`);
});
});Coin Selection
Select UTxOs for a transaction:
/**
* Simple coin selection algorithm using largest-first strategy.
* Selects UTxOs until we have enough to cover the target amount plus fees.
*
* @param utxos - Available UTxOs to select from
* @param targetLovelace - Amount needed (in lovelace, 1 ADA = 1,000,000 lovelace)
* @returns Object with selected UTxOs, total value, and change amount
*/
function selectUtxos(utxos, targetLovelace) {
// Sort by value descending - selecting largest UTxOs first
// minimizes the number of inputs (reduces tx size and fees)
const sorted = [...utxos].sort(
(a, b) => b.value.ada.lovelace - a.value.ada.lovelace
);
const selected = [];
let total = 0;
for (const utxo of sorted) {
selected.push(utxo);
total += utxo.value.ada.lovelace;
// Stop when we have enough for target + estimated fee buffer
// 200000 lovelace (~0.2 ADA) covers most simple transaction fees
if (total >= targetLovelace + 200000) {
break;
}
}
// Verify we actually have enough funds
if (total < targetLovelace) {
throw new Error('Insufficient funds');
}
return {
selected, // UTxOs to use as transaction inputs
total, // Total lovelace from selected UTxOs
change: total - targetLovelace // Amount to return to sender
};
}Performance Tips
Use WebSocket for Multiple Queries
For wallets or dApps querying many addresses:
// Single WebSocket connection for all queries (more efficient than HTTP)
const ws = new WebSocket(
`wss://api.nacho.builders/v1/ogmios?apikey=${API_KEY}`
);
ws.onopen = () => {
// Send all queries immediately (pipelining)
// No need to wait for responses between requests
addresses.forEach((addr, i) => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'queryLedgerState/utxo',
params: { addresses: [addr] },
id: `query-${i}` // Unique ID to match responses
}));
});
};
// Collect results as they arrive (may be out of order)
const results = new Map();
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Store result using the request ID
results.set(data.id, data.result);
// Check if we've received all responses
if (results.size === addresses.length) {
ws.close(); // Done - close connection
processAllUtxos(results); // Process collected UTxOs
}
};Cache Results
UTxOs don't change until spent. Cache results and invalidate on new blocks:
// Simple in-memory cache with timestamps
const utxoCache = new Map();
/**
* Get UTxOs with caching to reduce API calls.
* Cache is valid for 30 seconds (roughly 1-2 blocks).
*/
async function getCachedUtxos(address) {
const cached = utxoCache.get(address);
// Return cached data if fresh (less than 30 seconds old)
if (cached && Date.now() - cached.timestamp < 30000) {
return cached.utxos;
}
// Cache miss or stale - fetch fresh data
const utxos = await getAddressUtxos(address);
// Store in cache with timestamp for TTL checking
utxoCache.set(address, { utxos, timestamp: Date.now() });
return utxos;
}For real-time updates, use chain synchronization to detect when your UTxOs are spent. See the Chain Synchronization Guide.
Was this page helpful?