Beginner8 min read
Edit on GitHub

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

NetworkEndpoint
Mainnethttps://api.nacho.builders/v1/ogmios
Preprodhttps://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 unspent

UTxO 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)
}
FieldDescription
transaction.idThe transaction hash that created this UTxO
indexOutput index within the transaction
addressThe address holding this UTxO
value.ada.lovelaceADA amount in lovelace (1 ADA = 1,000,000 lovelace)
value.[policyId]Native tokens by policy ID and asset name
datumInline datum in CBOR hex (Plutus V2+)
datumHashHash of datum (Plutus V1 style)
scriptReference 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?