Mesh LogoMesh

Transaction Builder Basics

Build and customize Cardano transactions with the MeshTxBuilder low-level API.

Overview

MeshTxBuilder is a powerful, low-level API for constructing Cardano transactions. It provides fine-grained control over every aspect of transaction building—from simple ADA transfers to complex multi-signature and smart contract interactions.

When to use MeshTxBuilder:

  • Building custom transaction logic beyond standard transfers
  • Creating multi-signature transactions
  • Interacting with Plutus smart contracts
  • Optimizing transaction fees and execution units

Quick Start

import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";

// Initialize provider and builder
const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  verbose: true,
});

// Build a simple transfer
const unsignedTx = await txBuilder
  .txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);

Configuration

Constructor Options

const txBuilder = new MeshTxBuilder({
  fetcher: provider,      // Required for auto-fetching UTXO data
  submitter: provider,    // Optional: for direct submission via builder
  evaluator: provider,    // Optional: for script execution unit optimization
  serializer: serializer, // Optional: custom serializer (default: CSLSerializer)
  params: protocolParams, // Optional: custom protocol parameters
  isHydra: false,         // Optional: use Hydra protocol parameters
  verbose: true,          // Optional: enable detailed logging
});
OptionTypeRequiredDescription
fetcherIFetcherRecommendedProvider for fetching blockchain data
submitterISubmitterNoProvider for submitting transactions
evaluatorIEvaluatorNoProvider for evaluating script execution
serializerIMeshTxSerializerNoCustom transaction serializer
paramsPartial<Protocol>NoCustom protocol parameters
isHydrabooleanNoEnable Hydra-specific parameters
verbosebooleanNoEnable detailed console logging

Sending ADA

Basic Transfer

Send ADA to a recipient address:

import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";

const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  verbose: true,
});

// Get wallet data
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();

// Build transaction
const unsignedTx = await txBuilder
  .txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);

Send Multiple Outputs

Send to multiple recipients in a single transaction:

const unsignedTx = await txBuilder
  .txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
  .txOut("addr_test1qr...", [{ unit: "lovelace", quantity: "3000000" }])
  .txOut("addr_test1qs...", [{ unit: "lovelace", quantity: "2000000" }])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Send Native Assets

Include native assets (tokens/NFTs) in the output:

const unsignedTx = await txBuilder
  .txOut("addr_test1qz...", [
    { unit: "lovelace", quantity: "2000000" },
    { unit: "d9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e", quantity: "1" }
  ])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Input Selection

Automatic Selection

Let Mesh automatically select UTXOs to cover the transaction:

const unsignedTx = await txBuilder
  .txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Selection Strategies

Configure how UTXOs are selected:

txBuilder.selectUtxosFrom(
  utxos,           // Available UTXOs
  "largestFirst",  // Selection strategy
  "5000000",       // Minimum threshold (lovelace)
  true             // Include transaction fees in calculation
);
StrategyDescription
experimentalMesh's optimized selection algorithm
keepRelevantPrefer UTXOs with relevant assets
largestFirstSelect largest UTXOs first
largestFirstMultiAssetOptimized for transactions with multiple assets

Manual Input Selection

Explicitly specify which UTXOs to spend:

const unsignedTx = await txBuilder
  .txIn(
    "abc123...",  // Transaction hash
    0,            // Output index
    [{ unit: "lovelace", quantity: "10000000" }], // Amount
    "addr_test1qz..." // Address
  )
  .txOut("addr_test1qr...", [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(changeAddress)
  .complete();

Multi-Signature Transactions

Create transactions requiring multiple signatures:

import { MeshTxBuilder, ForgeScript, resolveScriptHash, stringToHex } from "@meshsdk/core";
import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";

// Create minting wallet
const mintingWallet = await MeshCardanoHeadlessWallet.fromMnemonic({
  networkId: 0,
  walletAddressType: AddressType.Base,
  fetcher: provider,
  submitter: provider,
  mnemonic: ["your", "mnemonic", "words", "..."],
});

// Create forging script
const forgingScript = ForgeScript.withOneSignature(
  await mintingWallet.getChangeAddressBech32()
);

const policyId = resolveScriptHash(forgingScript);
const assetName = "MeshToken";

// Get user wallet data
const address = (await wallet.getUsedAddresses())[0];
const utxos = await wallet.getUtxos();

// Build multi-sig transaction
const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  verbose: true,
});

const unsignedTx = await txBuilder
  .mint("1", policyId, stringToHex(assetName))
  .mintingScript(forgingScript)
  .metadataValue(721, {
    [policyId]: {
      [assetName]: { name: "Mesh Token", image: "ipfs://..." }
    }
  })
  .changeAddress(address)
  .selectUtxosFrom(utxos)
  .complete();

// Sign with both wallets (partial signing)
const signedTx1 = await wallet.signTx(unsignedTx, true, false);
const signedTx2 = await mintingWallet.signTx(signedTx1, true, false);

// Submit final transaction
const txHash = await wallet.submitTx(signedTx2);

Native Script Multi-Sig

Spend from a native script address requiring multiple signatures:

import { deserializeAddress, serializeNativeScript, NativeScript } from "@meshsdk/core";

// Get key hashes from addresses
const { pubKeyHash: keyHash1 } = deserializeAddress(walletAddress1);
const { pubKeyHash: keyHash2 } = deserializeAddress(walletAddress2);

// Create native script requiring both signatures
const nativeScript: NativeScript = {
  type: "all",
  scripts: [
    { type: "sig", keyHash: keyHash1 },
    { type: "sig", keyHash: keyHash2 },
  ],
};

// Serialize script to get address
const { address: scriptAddress, scriptCbor } = serializeNativeScript(nativeScript);

// Fetch UTXOs from script address
const scriptUtxos = await provider.fetchAddressUTxOs(scriptAddress);
const utxo = scriptUtxos[0];

// Build transaction spending from script
const unsignedTx = await txBuilder
  .txIn(
    utxo.input.txHash,
    utxo.input.outputIndex,
    utxo.output.amount,
    utxo.output.address
  )
  .txInScript(scriptCbor)
  .txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "2000000" }])
  .changeAddress(scriptAddress)
  .selectUtxosFrom(scriptUtxos)
  .complete();

// Both wallets must sign
const signedTx1 = await wallet1.signTx(unsignedTx, true, false);
const signedTx2 = await wallet2.signTx(signedTx1, true, false);
const txHash = await wallet1.submitTx(signedTx2);

Transaction Metadata

Add Messages

Attach messages to transactions using label 674 (CIP-20):

const unsignedTx = await txBuilder
  .txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
  .metadataValue(674, {
    msg: [
      "Invoice-No: 1234567890",
      "Customer-No: 555-1234"
    ]
  })
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Each message string must be at most 64 bytes when UTF-8 encoded.

Custom Metadata

Add any JSON-compatible metadata:

const unsignedTx = await txBuilder
  .metadataValue(customLabel, {
    key: "value",
    nested: { data: [1, 2, 3] }
  })
  .txOut(address, amount)
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Transaction Validity

Set Expiration (TTL)

Make a transaction invalid after a specific slot:

import { resolveSlotNo } from "@meshsdk/core";

// Transaction expires in 5 minutes
const expirationTime = new Date(Date.now() + 5 * 60 * 1000);
const slot = resolveSlotNo("mainnet", expirationTime.getTime());

const unsignedTx = await txBuilder
  .txOut(address, amount)
  .invalidHereafter(Number(slot))
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Set Start Time

Make a transaction invalid before a specific slot:

const startTime = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now
const startSlot = resolveSlotNo("mainnet", startTime.getTime());

const unsignedTx = await txBuilder
  .txOut(address, amount)
  .invalidBefore(Number(startSlot))
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Required Signers

Specify required signers for smart contract transactions:

import { deserializeAddress } from "@meshsdk/core";

const { pubKeyHash } = deserializeAddress(walletAddress);

const unsignedTx = await txBuilder
  // ... transaction details
  .requiredSignerHash(pubKeyHash)
  .complete();

Network Configuration

Set Network

Configure the network for proper cost model calculation:

txBuilder.setNetwork("mainnet"); // or "testnet", "preview", "preprod"

Custom Protocol Parameters

Use specific protocol parameters:

const params = await provider.fetchProtocolParameters();

const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  params: params,
});

Manual Fee Control

Override automatic fee calculation:

const unsignedTx = await txBuilder
  .txOut(address, amount)
  .changeAddress(changeAddress)
  .setFee("200000") // Set exact fee in lovelace
  .complete();

Build with JSON

Alternative method using a JSON object:

import { MeshTxBuilderBody } from "@meshsdk/core";

const meshTxBody: Partial<MeshTxBuilderBody> = {
  outputs: [
    {
      address: recipientAddress,
      amount: [{ unit: "lovelace", quantity: "5000000" }],
    },
  ],
  changeAddress: changeAddress,
  extraInputs: utxos,
  selectionConfig: {
    threshold: "5000000",
    strategy: "largestFirst",
    includeTxFees: true,
  },
};

const unsignedTx = await txBuilder.complete(meshTxBody);

Complete Example

import {
  MeshTxBuilder,
  BlockfrostProvider,
  resolveSlotNo
} from "@meshsdk/core";

// Setup
const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  verbose: true,
});

// Get wallet data
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();

// Set expiration (5 minutes)
const expiration = new Date(Date.now() + 5 * 60 * 1000);
const expirationSlot = resolveSlotNo("preprod", expiration.getTime());

// Build transaction
const unsignedTx = await txBuilder
  // Send 5 ADA to recipient
  .txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
  // Send 3 ADA + token to another recipient
  .txOut("addr_test1qr...", [
    { unit: "lovelace", quantity: "3000000" },
    { unit: "policyId...", quantity: "1" }
  ])
  // Add transaction message
  .metadataValue(674, { msg: ["Payment for services"] })
  // Set expiration
  .invalidHereafter(Number(expirationSlot))
  // Configure change and UTXO selection
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos, "largestFirst", "5000000", true)
  // Build the transaction
  .complete();

// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);

console.log(`Transaction submitted: ${txHash}`);

Troubleshooting

"Insufficient funds" error

  • Ensure UTXOs have enough ADA to cover outputs + fees + min UTxO values
  • Check that selectUtxosFrom is provided with current UTXOs
  • For native asset outputs, ensure minimum ADA (typically ~1.5 ADA) is included

"Missing input" error

  • The fetcher provider is required for automatic UTXO data fetching
  • If using manual txIn, provide all four parameters (txHash, index, amount, address)

Transaction too large

  • Split into multiple transactions
  • Reduce the number of outputs
  • Use UTXO consolidation first

Invalid slot number

  • Ensure you're using the correct network in resolveSlotNo
  • Mainnet and testnets have different slot calculations

Multi-sig transaction fails

  • Ensure partialSign: true is set for all signers except the last
  • Verify all required signers have signed
  • Check that signing order doesn't matter (unless script requires specific order)

On this page