Mesh LogoMesh

Build a Custom Blockchain Provider

Create custom providers to connect Mesh SDK to any data source. Implement IFetcher and ISubmitter interfaces for your own infrastructure.

Overview

In this guide, you build a custom provider class that connects Mesh SDK to your own blockchain data source. Custom providers let you use GraphQL endpoints, cardano-cli, websockets, or your own infrastructure while maintaining full compatibility with Mesh's transaction builder.

What you will build

  • A TypeScript provider class implementing IFetcher and ISubmitter
  • Methods to fetch UTxOs, protocol parameters, and account info
  • Transaction submission to the Cardano network

What you will learn

  • How provider interfaces work in Mesh SDK
  • Data mapping between external APIs and Mesh types
  • Error handling patterns for blockchain queries

Prerequisites

  • TypeScript project set up with Mesh SDK
  • Understanding of your target data source's API
  • Basic knowledge of Cardano data structures

Time to complete

60 minutes

Quick Start

Here is a minimal custom provider template:

import type {
  AccountInfo,
  AssetMetadata,
  Protocol,
  UTxO,
} from "@meshsdk/core";
import { IFetcher, ISubmitter } from "@meshsdk/core";

export class CustomProvider implements IFetcher, ISubmitter {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async fetchAccountInfo(address: string): Promise<AccountInfo> {
    // Implement your logic
    throw new Error("Not implemented");
  }

  async fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]> {
    // Implement your logic
    throw new Error("Not implemented");
  }

  async fetchAssetAddresses(asset: string): Promise<{ address: string; quantity: string }[]> {
    throw new Error("Not implemented");
  }

  async fetchAssetMetadata(asset: string): Promise<AssetMetadata> {
    throw new Error("Not implemented");
  }

  async fetchHandleAddress(handle: string): Promise<string> {
    throw new Error("Not implemented");
  }

  async fetchProtocolParameters(epoch?: number): Promise<Protocol> {
    // Implement your logic
    throw new Error("Not implemented");
  }

  async submitTx(tx: string): Promise<string> {
    // Implement your logic
    throw new Error("Not implemented");
  }
}

Step-by-Step Guide

Step 1: Understand the interfaces

Provider interfaces define the contract your class must follow. Any class implementing an interface must include all specified methods with matching signatures.

IFetcher interface - Query blockchain data:

import type {
  AccountInfo,
  AssetMetadata,
  Protocol,
  UTxO,
} from "@meshsdk/core";

export interface IFetcher {
  fetchAccountInfo(address: string): Promise<AccountInfo>;
  fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]>;
  fetchAssetAddresses(asset: string): Promise<{ address: string; quantity: string }[]>;
  fetchAssetMetadata(asset: string): Promise<AssetMetadata>;
  fetchHandleAddress(handle: string): Promise<string>;
  fetchProtocolParameters(epoch?: number): Promise<Protocol>;
}

ISubmitter interface - Submit transactions:

export interface ISubmitter {
  submitTx(tx: string): Promise<string>;
}

What to expect: Your provider must implement all methods from the interfaces you choose to implement.

Step 2: Create the provider class

Create a new file src/providers/custom-provider.ts:

import type {
  AccountInfo,
  AssetMetadata,
  Protocol,
  UTxO,
} from "@meshsdk/core";
import { IFetcher, ISubmitter } from "@meshsdk/core";
import axios, { AxiosInstance } from "axios";

export class CustomProvider implements IFetcher, ISubmitter {
  private readonly axiosInstance: AxiosInstance;

  constructor(baseUrl: string, apiKey?: string) {
    this.axiosInstance = axios.create({
      baseURL: baseUrl,
      headers: apiKey ? { "Authorization": `Bearer ${apiKey}` } : {},
    });
  }

  // Methods will be implemented in the following steps
}

Install axios if you have not already:

npm install axios

What to expect: A provider class with HTTP client configured and ready for method implementation.

Step 3: Implement fetchAddressUTxOs

This method retrieves all UTxOs for an address. It is the most commonly used fetcher method.

async fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]> {
  try {
    // Example: query your API for UTxOs
    const { data } = await this.axiosInstance.get(`/addresses/${address}/utxos`, {
      params: asset ? { asset } : {},
    });

    // Map your API response to Mesh's UTxO type
    return data.map((utxo: any): UTxO => ({
      input: {
        outputIndex: utxo.output_index,
        txHash: utxo.tx_hash,
      },
      output: {
        address: utxo.address,
        amount: utxo.amount.map((a: any) => ({
          unit: a.unit,
          quantity: a.quantity,
        })),
        dataHash: utxo.data_hash || undefined,
        plutusData: utxo.inline_datum || undefined,
        scriptRef: utxo.reference_script || undefined,
        scriptHash: utxo.script_hash || undefined,
      },
    }));
  } catch (error) {
    console.error("Failed to fetch UTxOs:", error);
    throw error;
  }
}

What to expect: An array of UTxO objects with the correct structure for Mesh.

Step 4: Implement fetchProtocolParameters

This method retrieves current protocol parameters used for transaction building.

async fetchProtocolParameters(epoch?: number): Promise<Protocol> {
  try {
    const { data } = await this.axiosInstance.get("/protocol-parameters", {
      params: epoch ? { epoch } : {},
    });

    // Map your API response to Mesh's Protocol type
    return {
      coinsPerUTxOSize: data.coins_per_utxo_size,
      collateralPercent: data.collateral_percent,
      decentralisation: data.decentralisation || 0,
      epoch: data.epoch_no,
      keyDeposit: data.key_deposit,
      maxBlockExMem: data.max_block_ex_mem.toString(),
      maxBlockExSteps: data.max_block_ex_steps.toString(),
      maxBlockHeaderSize: data.max_bh_size,
      maxBlockSize: data.max_block_size,
      maxCollateralInputs: data.max_collateral_inputs,
      maxTxExMem: data.max_tx_ex_mem.toString(),
      maxTxExSteps: data.max_tx_ex_steps.toString(),
      maxTxSize: data.max_tx_size,
      maxValSize: data.max_val_size.toString(),
      minFeeA: data.min_fee_a,
      minFeeB: data.min_fee_b,
      minPoolCost: data.min_pool_cost,
      poolDeposit: data.pool_deposit,
      priceMem: data.price_mem,
      priceStep: data.price_step,
    };
  } catch (error) {
    console.error("Failed to fetch protocol parameters:", error);
    throw error;
  }
}

What to expect: A Protocol object with all required fields for transaction building.

Step 5: Implement fetchAccountInfo

This method retrieves staking information for an address.

async fetchAccountInfo(address: string): Promise<AccountInfo> {
  try {
    const { data } = await this.axiosInstance.get(`/accounts/${address}`);

    return {
      active: data.active,
      balance: data.controlled_amount,
      rewards: data.withdrawable_amount,
      poolId: data.pool_id || undefined,
    };
  } catch (error) {
    console.error("Failed to fetch account info:", error);
    throw error;
  }
}

What to expect: Account staking details including balance and rewards.

Step 6: Implement submitTx

This method submits signed transactions to the network.

async submitTx(tx: string): Promise<string> {
  try {
    const { data, status } = await this.axiosInstance.post(
      "/tx/submit",
      tx,
      {
        headers: {
          "Content-Type": "application/cbor",
        },
      }
    );

    if (status === 200 || status === 202) {
      return data.tx_hash || data;
    }

    throw new Error(`Submission failed with status ${status}`);
  } catch (error: any) {
    if (error.response?.data) {
      throw new Error(`Transaction rejected: ${JSON.stringify(error.response.data)}`);
    }
    throw error;
  }
}

What to expect: The transaction hash on successful submission.

Step 7: Implement remaining methods

Complete the provider with the remaining interface methods:

async fetchAssetAddresses(asset: string): Promise<{ address: string; quantity: string }[]> {
  try {
    const { data } = await this.axiosInstance.get(`/assets/${asset}/addresses`);

    return data.map((item: any) => ({
      address: item.address,
      quantity: item.quantity,
    }));
  } catch (error) {
    console.error("Failed to fetch asset addresses:", error);
    throw error;
  }
}

async fetchAssetMetadata(asset: string): Promise<AssetMetadata> {
  try {
    const { data } = await this.axiosInstance.get(`/assets/${asset}`);

    return {
      ...data.onchain_metadata,
    };
  } catch (error) {
    console.error("Failed to fetch asset metadata:", error);
    throw error;
  }
}

async fetchHandleAddress(handle: string): Promise<string> {
  try {
    // ADA Handle resolution - format handle name without $
    const handleName = handle.replace(/^\$/, "");
    const { data } = await this.axiosInstance.get(`/handles/${handleName}`);

    return data.address;
  } catch (error) {
    console.error("Failed to fetch handle address:", error);
    throw error;
  }
}

What to expect: A complete provider implementing all required methods.

Complete Example

Here is the complete custom provider implementation:

import type {
  AccountInfo,
  AssetMetadata,
  Protocol,
  UTxO,
} from "@meshsdk/core";
import { IFetcher, ISubmitter } from "@meshsdk/core";
import axios, { AxiosInstance } from "axios";

export class CustomProvider implements IFetcher, ISubmitter {
  private readonly axiosInstance: AxiosInstance;

  constructor(baseUrl: string, apiKey?: string) {
    this.axiosInstance = axios.create({
      baseURL: baseUrl,
      headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
      timeout: 30000,
    });
  }

  async fetchAccountInfo(address: string): Promise<AccountInfo> {
    try {
      const { data } = await this.axiosInstance.get(`/accounts/${address}`);
      return {
        active: data.active,
        balance: data.controlled_amount,
        rewards: data.withdrawable_amount,
        poolId: data.pool_id || undefined,
      };
    } catch (error) {
      console.error("Failed to fetch account info:", error);
      throw error;
    }
  }

  async fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]> {
    try {
      const { data } = await this.axiosInstance.get(`/addresses/${address}/utxos`, {
        params: asset ? { asset } : {},
      });

      return data.map((utxo: any): UTxO => ({
        input: {
          outputIndex: utxo.output_index,
          txHash: utxo.tx_hash,
        },
        output: {
          address: utxo.address,
          amount: utxo.amount.map((a: any) => ({
            unit: a.unit,
            quantity: a.quantity,
          })),
          dataHash: utxo.data_hash || undefined,
          plutusData: utxo.inline_datum || undefined,
          scriptRef: utxo.reference_script || undefined,
          scriptHash: utxo.script_hash || undefined,
        },
      }));
    } catch (error) {
      console.error("Failed to fetch UTxOs:", error);
      throw error;
    }
  }

  async fetchAssetAddresses(
    asset: string
  ): Promise<{ address: string; quantity: string }[]> {
    try {
      const { data } = await this.axiosInstance.get(`/assets/${asset}/addresses`);
      return data.map((item: any) => ({
        address: item.address,
        quantity: item.quantity,
      }));
    } catch (error) {
      console.error("Failed to fetch asset addresses:", error);
      throw error;
    }
  }

  async fetchAssetMetadata(asset: string): Promise<AssetMetadata> {
    try {
      const { data } = await this.axiosInstance.get(`/assets/${asset}`);
      return { ...data.onchain_metadata };
    } catch (error) {
      console.error("Failed to fetch asset metadata:", error);
      throw error;
    }
  }

  async fetchHandleAddress(handle: string): Promise<string> {
    try {
      const handleName = handle.replace(/^\$/, "");
      const { data } = await this.axiosInstance.get(`/handles/${handleName}`);
      return data.address;
    } catch (error) {
      console.error("Failed to fetch handle address:", error);
      throw error;
    }
  }

  async fetchProtocolParameters(epoch?: number): Promise<Protocol> {
    try {
      const { data } = await this.axiosInstance.get("/protocol-parameters", {
        params: epoch ? { epoch } : {},
      });

      return {
        coinsPerUTxOSize: data.coins_per_utxo_size,
        collateralPercent: data.collateral_percent,
        decentralisation: data.decentralisation || 0,
        epoch: data.epoch_no,
        keyDeposit: data.key_deposit,
        maxBlockExMem: data.max_block_ex_mem.toString(),
        maxBlockExSteps: data.max_block_ex_steps.toString(),
        maxBlockHeaderSize: data.max_bh_size,
        maxBlockSize: data.max_block_size,
        maxCollateralInputs: data.max_collateral_inputs,
        maxTxExMem: data.max_tx_ex_mem.toString(),
        maxTxExSteps: data.max_tx_ex_steps.toString(),
        maxTxSize: data.max_tx_size,
        maxValSize: data.max_val_size.toString(),
        minFeeA: data.min_fee_a,
        minFeeB: data.min_fee_b,
        minPoolCost: data.min_pool_cost,
        poolDeposit: data.pool_deposit,
        priceMem: data.price_mem,
        priceStep: data.price_step,
      };
    } catch (error) {
      console.error("Failed to fetch protocol parameters:", error);
      throw error;
    }
  }

  async submitTx(tx: string): Promise<string> {
    try {
      const { data, status } = await this.axiosInstance.post("/tx/submit", tx, {
        headers: { "Content-Type": "application/cbor" },
      });

      if (status === 200 || status === 202) {
        return data.tx_hash || data;
      }

      throw new Error(`Submission failed with status ${status}`);
    } catch (error: any) {
      if (error.response?.data) {
        throw new Error(`Transaction rejected: ${JSON.stringify(error.response.data)}`);
      }
      throw error;
    }
  }
}

Use the custom provider with Mesh:

import { MeshTxBuilder } from "@meshsdk/core";
import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";
import { CustomProvider } from "./providers/custom-provider";

const provider = new CustomProvider("https://your-api.com", "your-api-key");
const wallet = await MeshCardanoBrowserWallet.enable("eternl");

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

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

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

const signedTx = await wallet.signTx(unsignedTx);
const txHash = await provider.submitTx(signedTx);

Troubleshooting

Type mismatches in return values

Cause: Your API response structure differs from Mesh's expected types.

Solution: Check the type definitions in @meshsdk/core. Map each field explicitly in your implementation. Use TypeScript's strict mode to catch mismatches.

Network timeout errors

Cause: API requests take too long or the endpoint is unreachable.

Solution: Configure appropriate timeouts in your axios instance. Implement retry logic for transient failures:

import axiosRetry from "axios-retry";

axiosRetry(this.axiosInstance, {
  retries: 3,
  retryDelay: axiosRetry.exponentialDelay,
});

Transaction submission returns error

Cause: The transaction is malformed or fails validation.

Solution: Parse the error response from your API. Common issues include insufficient funds, missing collateral, or script validation failures. Log the full error for debugging.

Missing optional fields cause errors

Cause: Your API does not return all fields that Mesh expects.

Solution: Use optional chaining and provide defaults:

dataHash: utxo.data_hash || undefined,
scriptRef: utxo.reference_script ?? undefined,

Rate limiting from your API

Cause: Too many requests in a short period.

Solution: Implement request queuing or caching for frequently accessed data like protocol parameters:

private protocolParamsCache: { params: Protocol; timestamp: number } | null = null;
private CACHE_TTL = 60000; // 1 minute

async fetchProtocolParameters(epoch?: number): Promise<Protocol> {
  if (this.protocolParamsCache &&
      Date.now() - this.protocolParamsCache.timestamp < this.CACHE_TTL) {
    return this.protocolParamsCache.params;
  }

  const params = await this.fetchProtocolParametersFromApi(epoch);
  this.protocolParamsCache = { params, timestamp: Date.now() };
  return params;
}

On this page