Apibara

apibara.com
Developer Tools

Apibara is the fastest platform to build production-grade indexers that connect onchain data to web2 services.

llms.txt

This page contains the Apibara documentation as a single document for consumption by LLMs.


title: Apibara documentation titleShort: Overview description: "Welcome to the Apibara documentation. Find more information about the Apibara protocol." priority: 1000 fullpage: true


title: Installation description: "Learn how to install and get started with Apibara." diataxis: tutorial updatedAt: 2025-06-11

Installation

This tutorial shows how to setup an Apibara project from scratch. The goal is to start indexing data as quickly as possible and to understand the basic structure of a project. By the end of this tutorial, you will have a basic indexer that streams data from two networks (Ethereum and Starknet).

Installation

This tutorial starts with a fresh Typescript project. In the examples, we use pnpm as the package manager, but you can use any package manager you prefer.

Let's start by creating the project. The --language flag specifies which language to use to implement indexers, while the --no-create-indexer flag is used to delay the creation of the indexer.

:::cli-command

mkdir my-indexer
cd my-indexer
pnpm dlx apibara@next init . --language="ts" --no-create-indexer
ℹ Initializing project in .
✔ Created  package.json
✔ Created  tsconfig.json
✔ Created  apibara.config.ts

✔ Project initialized successfully

:::

After that, you can install the dependencies.

pnpm install

Apibara Config

Your indexers' configuration goes in the apibara.config.ts file. You can leave the configuration as is for now.

import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {},
});

API Key

The streams hosted by Apibara require an API key.

EVM Indexer

Let's create the first EVM indexer. All indexers must go in the indexers directory and have a name that ends with .indexer.ts or .indexer.js. The Apibara CLI will automatically detect the indexers in this directory and make them available to the project.

You can use the apibara add command to add an indexer to your project. This command does the following:

  • gathers information about the chain you want to index.
  • asks about your preferred storage solution.
  • creates the indexer.
  • adds dependencies to your package.json.

:::cli-command

pnpm apibara add
✔ Indexer ID: … rocket-pool
✔ Select a chain: › Ethereum
✔ Select a network: › Mainnet
✔ Select a storage: › None
✔ Updated apibara.config.ts
✔ Updated package.json
✔ Created rocket-pool.indexer.ts

ℹ Before running the indexer, run pnpm run install & pnpm run prepare

:::

After installing dependencies, you can look at the changes to apibara.config.ts. Notice the indexer's specific runtime configuration. This is a good time to update the indexing starting block.

import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {
    rocketPool: {
      startingBlock: 21_000_000,
      streamUrl: "https://mainnet.ethereum.a5a.ch",
    },
  },
});

Implement the indexer by editing indexers/rocket-pool.indexer.ts.

import { defineIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";

import { EvmStream } from "@apibara/evm";
import type { ApibaraRuntimeConfig } from "apibara/types";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  const { startingBlock, streamUrl } = runtimeConfig.rocketPool;

  return defineIndexer(EvmStream)({
    streamUrl,
    finality: "accepted",
    startingBlock: BigInt(startingBlock),
    filter: {
      logs: [
        {
          address: "0xae78736Cd615f374D3085123A210448E74Fc6393",
        },
      ],
    },
    plugins: [],
    async transform({ block }) {
      const logger = useLogger();
      const { logs, header } = block;
      logger.log(`Block number ${header?.blockNumber}`);
      for (const log of logs) {
        logger.log(
          `Log ${log.logIndex} from ${log.address} tx=${log.transactionHash}`
        );
      }
    },
  });
}

Notice the following:

  • The indexer file exports a single indexer.
  • The defineIndexer function takes the stream as parameter. In this case, the EvmStream is used. This is needed because Apibara supports multiple networks with different data types.
  • streamUrl specifies where the data comes from. You can connect to streams hosted by us, or to self-hosted streams.
  • startingBlock specifies from which block to start streaming.
  • These two properties are read from the runtimeConfig object. Use the runtime configuration object to have multiple presets for the same indexer.
  • The filter specifies which data to receive. You can read more about the available data for EVM chains in the EVM documentation.
  • The transform function is called for each block. It receives the block as parameter. This is where your indexer processes the data.
  • The useLogger hook returns an indexer-specific logger.

There are more indexer options available, you can find them in the documentation.

Running the indexer

During development, you will use the apibara CLI to build and run indexers. For convenience, the template adds the following scripts to your package.json:

{
  "scripts": {
    "dev": "apibara dev",
    "build": "apibara build",
    "start": "apibara start"
  }
}
  • dev: runs all indexers in development mode. Indexers are automatically reloaded and restarted when they change.
  • build: builds the indexers for production.
  • start: runs a single indexer in production mode. Notice you must first build the indexers.

Before running the indexer, you must set the DNA_TOKEN environment variable to your DNA API key, created from the dashboard. You can store the environment variable in a .env file, but make sure not to commit it to git!

Now, run the indexer in development mode.

:::cli-command

pnpm run dev
> apibara-app@0.1.0 dev /tmp/my-indexer
> apibara dev

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 19369 ms
✔ Restarting indexers
rocket-pool | log Block number 21000071
rocket-pool | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool | log Block number 21000097
rocket-pool | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool | log Block number 21000111
rocket-pool | log Log 589 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xa01ec6551e76364f6cf687f52823d66b1c07f7a47ce157a9cd9e441691a021f0
...

:::

Starknet indexer

You can index data on different networks in the same project. Let's add an indexer for Starknet. Like before, you can use the apibara add command to add an indexer to your project.

:::cli-command

pnpm apibara add
✔ Indexer ID: … strk-staking
✔ Select a chain: › Starknet
✔ Select a network: › Mainnet
✔ Select a storage: › None
✔ Updated apibara.config.ts
✔ Updated package.json
✔ Created strk-staking.indexer.ts

ℹ Before running the indexer, run pnpm run install & pnpm run prepare

:::

After that, you can implement the indexer. In this case, the indexer listens for all events emitted by the STRK staking contract.

Let's start by updating the apibara.config.ts file with the starting block.

// ...

export default defineConfig({
  runtimeConfig: {
    // ...
    strkStaking: {
      startingBlock: 900_000,
      streamUrl: "https://mainnet.starknet.a5a.ch",
    },
  },
  // ...
});

Then you can implement the indexer.

import { defineIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";

import type { ApibaraRuntimeConfig } from "apibara/types";
import { StarknetStream } from "@apibara/starknet";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  const { startingBlock, streamUrl } = runtimeConfig.strkStaking;

  return defineIndexer(StarknetStream)({
    streamUrl,
    finality: "accepted",
    startingBlock: BigInt(startingBlock),
    filter: {
      events: [
        {
          address:
            "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
        },
      ],
    },
    plugins: [],
    async transform({ block }) {
      const logger = useLogger();
      const { events, header } = block;
      logger.log(`Block number ${header?.blockNumber}`);
      for (const event of events) {
        logger.log(`Event ${event.eventIndex} tx=${event.transactionHash}`);
      }
    },
  });
}

You can now run the indexer. In this case, you can specify which indexer you want to run with the --indexers option. When the flag is omitted, all indexers are run concurrently.

:::cli-command

pnpm run dev --indexers strk-staking
...
> apibara-app@0.1.0 dev /tmp/my-indexer
> apibara dev "--indexers=strk-staking"

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 20072 ms
✔ Restarting indexers
strk-staking | log Block number 929092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 929119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7

:::

Production build

The apibara build command is used to build a production version of the indexer. There are two main changes for the production build:

  • No hot code reloading is available.
  • Only one indexer is started. If your project has multiple indexers, it should start them independently.

:::cli-command

pnpm run build
> apibara-app@0.1.0 build /tmp/my-indexer
> apibara build

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
◐ Building 2 indexers
✔ Build succeeded!
ℹ You can start the indexers with apibara start

:::

Once the indexers are built, you can run them in two (equivalent) ways:

  • The apibara start command by specifying which indexer to run with the --indexer flag. In this tutorial we are going to use this method.
  • Running .apibara/build/start.mjs with Node. This is useful when building Docker images for your indexers.

:::cli-command

pnpm run start --indexer rocket-pool
> apibara-app@0.1.0 start /tmp/my-indexer
> apibara start "--indexer" "rocket-pool"

◐ Starting indexer rocket-pool
rocket-pool  | log Block number 21000071
rocket-pool  | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool  | log Block number 21000097
rocket-pool  | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool  | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool  | log Block number 21000111
...

:::

Runtime configuration & presets

Apibara provides a mechanism for indexers to load their configuration from the apibara.config.ts file:

  • Add the configuration under the runtimeConfig key in apibara.config.ts.
  • Change your indexer's module to return a function that, given the runtime configuration, returns the indexer.

You can update the configuration to define values that are configurable by your indexer. This example used the runtime configuration to store the DNA stream URL and contract address.

import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {
    strkStaking: {
      startingBlock: 900_000,
      streamUrl: "https://mainnet.starknet.a5a.ch",
      contractAddress:
        "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
    },
  },
});

Then update the indexer to return a function that returns the indexer. Your editor is going to show a type error since the types of config.streamUrl and config.contractAddress are unknown, the next session is going to explain how to solve that issue.

import { defineIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";

import { StarknetStream } from "@apibara/starknet";
import { ApibaraRuntimeConfig } from "apibara/types";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  const config = runtimeConfig.strkStaking;
  const { startingBlock, streamUrl } = config;

  return defineIndexer(StarknetStream)({
    streamUrl,
    startingBlock: BigInt(startingBlock),
    filter: {
      events: [
        {
          address: config.contractAddress as `0x${string}`,
        },
      ],
    },
    async transform({ block }) {
      // Unchanged.
    },
  });
}

Typescript & type safety

You may have noticed that the CLI generates types in .apibara/types before building the indexers (both in development and production mode). These types contain the type definition of your runtime configuration. You can instruct Typescript to use them by adding the following tsconfig.json to your project.

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": ["**/*.ts", ".apibara/types"],
  "exclude": ["node_modules"]
}

After restarting the Typescript language server you will have a type-safe runtime configuration right into your indexer!

Presets

Having a single runtime configuration is useful but not enough for real-world indexers. The CLI provides a way to have multiple "presets" and select which one to use at runtime. This is useful, for example, if you're deploying the same indexers on multiple networks where only the DNA stream URL and contract addresses change.

You can have any number of presets in the configuration and use the --preset flag to select which one to use. For example, you can add a sepolia preset that contains the URL of the Starknet Sepolia DNA stream. If a preset doesn't specify a key, then the value from the root configuration is used.

import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {
    streamUrl: "https://mainnet.starknet.a5a.ch",
    contractAddress:
      "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a" as `0x${string}`,
  },
  presets: {
    sepolia: {
      runtimeConfig: {
        streamUrl: "https://sepolia.starknet.a5a.ch",
      },
    },
  },
});

You can then run the indexer in development mode using the sepolia preset.

:::cli-command

npm run dev -- --indexers=strk-staking --preset=sepolia
> my-indexer@1.0.0 dev
> apibara dev --indexers=strk-staking --preset=sepolia

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3858 ms
✔ Restarting indexers
strk-staking | log Block number 100092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 100119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
...

:::

Storing data & persisting state across restarts

All indexers implemented in this tutorial are stateless. They don't store any data to a database and if you restart them they will restart indexing from the beginning.

You can refer to our storage section to learn more about writing data to a database and persisting the indexer's state across restarts.


title: Indexers description: "Learn how to create indexers to stream and transform onchain data." diataxis: explanation updatedAt: 2025-01-05

Building indexers

Indexers are created using the defineIndexer higher-order function. This function takes a stream definition and returns a function to define the indexer.

The job of an indexer is to stream and process historical data (backfilling) and then switch to real-time mode. Indexers built using our SDK are designed to handle chain-reorganizations automatically. If, for any reason, you need to receive notifications about reorgs, you can define a custom message:invalidate hook to handle them.

By default, the indexer is stateless (restarts from the beginning on restart) and does not provide any storage. You can add persistence and storage by using one of the provided storage plugins.

Examples

The following examples show how to create indexers for the Beacon Chain, EVM (Ethereum), and Starknet.

Beacon Chain indexer

import { BeaconChainStream } from "@apibara/beaconchain";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(BeaconChainStream)({
  /* ... */
});

EVM (Ethereum) indexer

import { EvmStream } from "@apibara/evm";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(EvmStream)({
  /* ... */
});

Starknet indexer

import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(StarknetStream)({
  /* ... */
});

With runtime config

To configure the indexer at runtime, export a function that takes the configuration and returns the indexer's definition.

import { EvmStream } from "@apibara/evm";
import type { ApibaraRuntimeConfig } from "apibara/types";
import { defineIndexer } from "@apibara/indexer";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  return defineIndexer(EvmStream)({
    // ...
  });
}

Indexer configuration

All indexers take the same configuration options.

  • streamUrlstringThe URL of the DNA stream to connect to.
  • filterTFilterThe filter to apply to the DNA stream. This argument is specific to the stream definition. You should refer to the chain's filter reference for the available options (see Beacon Chain, EVM (Ethereum), Starknet).
  • finality"finalized" | "accepted" | "pending"Receive data with the specified finality. Defaults to accepted.
  • startingCursor{ orderKey: bigint, uniqueKey?: string }The cursor to start the indexer from. Defaults to the genesis block. The orderKey represents the block number, and the uniqueKey represents the block hash (optional).
  • debugbooleanEnable debug mode. This will print debug information to the console.
  • transform({ block, cursor, endCursor, finality, context }) => Promise<void>The transform function called for each block received from the DNA stream.
  • factory({ block, context }) => Promise<{ filter?: TFilter }>The factory function used to add data filters at runtime. Useful for creating indexers for smart contracts like Uniswap V2.
  • hooksobjectThe hooks to register with the indexer. Refer to the plugins & hooks page for more information.
  • pluginsarrayThe plugins to register with the indexer. Refer to the plugins & hooks page for more information.

The transform function

The transform function is invoked for each block received from the DNA stream. This function is where you should implement your business logic.

Arguments

  • blockTBlockThe block received from the DNA stream. This is chain-specific (see Beacon Chain, EVM (Ethereum), Starknet).
  • cursor{ orderKey: bigint, uniqueKey?: string }The cursor of the block before the received block.
  • endCursor{ orderKey: bigint, uniqueKey?: string }The cursor of the current block.
  • finality"finalized" | "accepted" | "pending"The finality of the block.
  • contextobjectThe context shared between the indexer and the plugins.

The following example shows a minimal indexer that streams block headers and prints them to the console.

import { EvmStream } from "@apibara/evm";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(EvmStream)({
  streamUrl: "https://mainnet.ethereum.a5a.ch",
  filter: {
    header: "always",
  },
  async transform({ block }) {
    const { header } = block;
    console.log(header);
  },
});

The factory function

The factory function is used to add data filters at runtime. This is useful for creating indexers for smart contracts that deploy other smart contracts like Uniswap V2 and its forks.

Arguments

  • blockTBlockThe block received from the DNA stream. This is chain-specific (see Beacon Chain, EVM (Ethereum), Starknet).
  • contextobjectThe context shared between the indexer and the plugins.

The following example shows a minimal indexer that streams PairCreated events from Uniswap V2 to detect new pools, and then streams the pool's events.

import { EvmStream } from "@apibara/evm";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(EvmStream)({
  streamUrl: "https://mainnet.ethereum.a5a.ch",
  filter: {
    logs: [
      {
        /* ... */
      },
    ],
  },
  async factory({ block }) {
    const { logs } = block;
    return {
      /* ... */
    };
  },
  async transform({ block }) {
    const { header, logs } = block;
    console.log(header);
    console.log(logs);
  },
});

title: Plugins & Hooks description: "Learn how to use plugins to extend the functionality of your indexers." diataxis: explanation updatedAt: 2025-01-05

Plugins & Hooks

Indexers are extensible through hooks and plugins. Hooks are functions that are called at specific points in the indexer's lifecycle. Plugins are components that contain reusable hooks callbacks.

Hooks

The following hooks are available in all indexers.

  • run:before() => void Called before the indexer starts running.
  • run:after() => void Called after the indexer has finished running.
  • connect:before({ request: StreamDataRequest<TFilter>, options: StreamDataOptions }) => void Called before the indexer connects to the DNA stream. Can be used to change the request or stream options.
  • connect:after({ request: StreamDataRequest<TFilter> }) => void Called after the indexer has connected to the DNA stream.
  • connect:factory({ request: StreamDataRequest<TFilter>, endCursor: { orderKey: bigint, uniqueKey?: string } }) => void Called before the indexer reconnects to the DNA stream with a new filter (in factory mode).
  • message({ message: StreamDataResponse<TBlock> }) => void Called for each message received from the DNA stream. Additionally, message-specific hooks are available: message:invalidate, message:finalize, message:heartbeat, message:systemMessage.
  • handler:middleware({ use: MiddlewareFunction) => void }) => void Called to register indexer's middlewares.

Using plugins

You can register plugins in the indexer's configuration, under the plugins key.

import { BeaconChainStream } from "@apibara/beaconchain";
import { defineIndexer } from "@apibara/indexer";

import { myAwesomePlugin } from "@/lib/my-plugin.ts";

export default defineIndexer(BeaconChainStream)({
  streamUrl: "https://beaconchain.preview.apibara.org",
  filter: { /* ... */ },
  plugins: [myAwesomePlugin()],
  async transform({ block: { header, validators } }) {
    /* ... */
  },
});

Building plugins

Developers can create new plugins to be shared across multiple indexers or projects. Plugins use the available hooks to extend the functionality of indexers.

The main way to define a plugin is by using the defineIndexerPlugin function. This function takes a callback with the indexer as parameter, the plugin should register itself with the indexer's hooks. When the runner runs the indexer, all the relevant hooks are called.

import type { Cursor } from "@apibara/protocol";
import { defineIndexerPlugin } from "@apibara/indexer/plugins";

export function myAwesomePlugin<TFilter, TBlock, TTxnParams>() {
  return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
    indexer.hooks.hook("connect:before", ({ request, options }) => {
      // Do something before the indexer connects to the DNA stream.
    });

    indexer.hooks.hook("run:after", () => {
      // Do something after the indexer has finished running.
    });
  });
}

Middleware

Apibara indexers support wrapping the transform function in middleware. This is used, for example, to wrap all database operations in a transaction.

The middleware is registered using the handler:middleware hook. This hook takes a use argument to register the middleware with the indexer. The argument to use is a function that takes the indexer's context and a next function to call the next middleware or the transform function.

import type { Cursor } from "@apibara/protocol";
import { defineIndexerPlugin } from "@apibara/indexer/plugins";

export function myAwesomePlugin<TFilter, TBlock, TTxnParams>() {
  return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
    const db = openDatabase();
    indexer.hooks.hook("handler:middleware", ({ use }) => {
      use(async (context, next) => {
        // Start a transaction.
        await db.transaction(async (txn) => {
          // Add the transaction to the context.
          context.db = txn;
          try {
            // Call the next middleware or the transform function.
            await next();
          } finally {
            // Remove the transaction from the context.
            context.db = undefined;
          }
        });
      });
    });
  });
}

Inline hooks

For all cases where you want to use a hook without creating a plugin, you can use the hooks property of the indexer.

IMPORTANT: inline hooks are the recommended way to add hooks to an indexer. If the same hook is needed in multiple indexers, it is better to create a plugin. Usually, plugins lives in the lib folder, for example lib/my-plugin.ts.

import { BeaconChainStream } from "@apibara/beaconchain";
import { defineIndexer } from "@apibara/indexer";

export default defineIndexer(BeaconChainStream)({
  streamUrl: "https://beaconchain.preview.apibara.org",
  filter: { /* ... */ },
  async transform({ block: { header, validators } }) {
    /* ... */
  },
  hooks: {
    async "connect:before"({ request, options }) {
      // Do something before the indexer connects to the DNA stream.
    },
  },
});

Indexer lifecycle

The following Javascript pseudocode shows the indexer's lifecycle. This should give you a good understanding of when hooks are called.

function run(indexer) {
  indexer.callHook("run:before");

  const { use, middleware } = registerMiddleware(indexer);

  indexer.callHook("handler:middleware", { use });

  // Create the request based on the indexer's configuration.
  const request = Request.create({
    filter: indexer.filter,
    startingCursor: indexer.startingCursor,
    finality: indexer.finality,
  });

  // Stream options.
  const options = {};

  indexer.callHook("connect:before", { request, options });

  let stream = indexer.streamData(request, options);

  indexer.callHook("connect:after");

  while (true) {
    const { message, done } = stream.next();

    if (done) {
      break;
    }

    indexer.callHook("message", { message });

    switch (message._tag) {
      case "data": {
        const { block, endCursor, finality } = message.data
        middleware(() => {
          if (indexer.isFactoryMode()) {
            // Handle the factory portion of the indexer data.
            // Implementation detail is not important here.
            const newFilter = indexer.factory();
            const request = Request.create(/* ... */);
  
            indexer.callHook("connect:factory", { request, endCursor });
            stream = indexer.streamData(request, options);
          }
  
          indexer.transform({ block, endCursor, finality });
        });
        break;
      }
      case "invalidate": {
        indexer.callHook("message:invalidate", { message });
        break;
      }
      case "finalize": {
        indexer.callHook("message:finalize", { message });
        break;
      }
      case "heartbeat": {
        indexer.callHook("message:heartbeat", { message });
        break;
      }
      case "systemMessage": {
        indexer.callHook("message:systemMessage", { message });
        break;
      }
    }
  }

  indexer.callHook("run:after");
}

title: Configuration - apibara.config.ts description: "Learn how to configure your indexers using the apibara.config.ts file." diataxis: reference updatedAt: 2025-03-11

apibara.config.ts

The apibara.config.ts file is where you configure your project. If the project is using Javascript, the file is named apibara.config.js.

General

runtimeConfig: R extends Record<string, unknown>

The runtimeConfig contains the runtime configuration passed to indexers if they accept it. Use this to configure chain or environment specific options such as starting block and contract address.

export default defineConfig({
  runtimeConfig: {
    connectionString: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://",
  },
});

presets: Record<string, R>

Presets represent different configurations of runtimeConfig. You can use presets to switch between different environments, like development, test, and production.

export default defineConfig({
  runtimeConfig: {
    connectionString: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://",
  },
  presets: {
    dev: {
      connectionString: "memory://",
    },
  },
});

preset: string

The default preset to use.

rootDir: string

Change the project's root directory.

buildDir: string

The directory used for building the indexers. Defaults to .apibara.

outputDir: string

The directory where to output the built indexers. Defaults to .apibara/build.

indexersDir: string

The directory where to look for *.indexer.ts or .indexer.js files. Defaults to indexers.

hooks

Project level hooks.

debug: boolean

Enable debug mode, printing more detailed logs.

Build config

rolldownConfig: RolldownConfig

Override any field in the Rolldown configuration.

exportConditions?: string[]

Shorthand for rolldownConfig.resolve.exportConditions.

File watcher

watchOptions: WatchOptions

Configure Rolldown's file watcher. Defaults to {ignore: ["**/node_modules/**", "**/.apibara/**"]}.


title: Instrumentation - instrumentation.ts description: "Learn how to send metrics and traces to your observability platform using instrumentation.ts." diataxis: reference updatedAt: 2025-03-19

instrumentation.ts

It's easy to add observability to your indexer using our native instrumentation powered by OpenTelemetry.

Convention

To add instrumentation to your project, create a instrumentation.ts file at the root (next to apibara.config.ts).

This file should export a register function, this function is called once by the runtime before the production indexer is started.

import type { RegisterFn } from "apibara/types";

export const register: RegisterFn = async () => {
  // Setup OpenTelemetry SDK exporter
};

Logger

You can also replace the default logger by exporting a logger function from instrumentation.ts. This function should return a console-like object with the same methods as console.

import type { LoggerFactoryFn } from "apibara/types";

export const logger: LoggerFactoryFn = ({ indexer, indexers, preset }) => {
  // Build console here.
};

Examples

OpenTelemetry with OpenTelemetry Collector

The OpenTelemetry Collector offers a vendor-agnostic implementation for receiving, processing, and exporting telemetry data. Using the collector with Apibara allows you to collect and send both metrics and traces to your preferred observability backend.

1. Install required dependencies

First, install the required OpenTelemetry packages:

pnpm install @opentelemetry/api @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-metrics-otlp-grpc @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/resources @opentelemetry/sdk-node @opentelemetry/sdk-metrics @opentelemetry/sdk-trace-node @opentelemetry/semantic-conventions

2. Update instrumentation.ts to use the OpenTelemetry Collector

Create or update the instrumentation.ts file at the root of your project:

import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import {
  defaultResource,
  resourceFromAttributes,
} from "@opentelemetry/resources";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import type { RegisterFn } from "apibara/types";

export const register: RegisterFn = async () => {
  // Create a resource that identifies your service
  const resource = defaultResource().merge(
    resourceFromAttributes({
      [ATTR_SERVICE_NAME]: "apibara",
      [ATTR_SERVICE_VERSION]: "1.0.0",
    })
  );
  const collectorOptions = {
    // configure the grpc endpoint of the opentelemetry-collector
    url: "http://localhost:4317",
  };

  // Configure the OTLP exporter for metrics using grpc protocol,
  const metricExporter = new OTLPMetricExporter(collectorOptions);
  const metricReader = new PeriodicExportingMetricReader({
    exporter: metricExporter,
    exportIntervalMillis: 1000,
  });

  // Configure the OTLP exporter for traces using grpc protocol,
  const traceExporter = new OTLPTraceExporter(collectorOptions);
  const spanProcessors = [new SimpleSpanProcessor(traceExporter)];

  // Configure the SDK
  const sdk = new NodeSDK({
    resource,
    spanProcessors,
    metricReader,
    instrumentations: [getNodeAutoInstrumentations()],
  });

  // Start the SDK
  sdk.start();
};

3. Build and run your indexer to see the metrics and traces on the configured OpenTelemetry Collector.

pnpm build
pnpm start --indexer=your-indexer

The following metrics are available out of the box:

  • current_block: The latest block number being processed by the indexer
  • processed_blocks_total: Total number of blocks that have been processed
  • reorgs_total: Number of chain reorganizations detected and handled

Additionally, you can observe detailed traces showing:

  • Block processing lifecycle and duration
  • Individual transform function execution time
  • Chain reorganization handling

Prometheus

Collecting metrics with Prometheus and visualizing them is a powerful way to monitor your indexers. This section shows how to set up OpenTelemetry with Prometheus.

1. Install required dependencies

First, install the required OpenTelemetry packages:

pnpm install @opentelemetry/api @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-prometheus @opentelemetry/resources @opentelemetry/sdk-node @opentelemetry/semantic-conventions

2. Update instrumentation.ts to use the OpenTelemetry SDK

Update the instrumentation.ts file at the root of your project:

import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import {
  defaultResource,
  resourceFromAttributes,
} from "@opentelemetry/resources";
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import type { RegisterFn } from "apibara/types";

export const register: RegisterFn = async () => {
  // Create a resource that identifies your service
  const resource = defaultResource().merge(
    resourceFromAttributes({
      [ATTR_SERVICE_NAME]: "apibara",
      [ATTR_SERVICE_VERSION]: "1.0.0",
    })
  );

  // By default port 9464 will be exposed on the apibara app
  const prometheusExporter = new PrometheusExporter();

  // Configure the SDK
  const sdk = new NodeSDK({
    resource,
    metricReader: prometheusExporter,
    instrumentations: [getNodeAutoInstrumentations()],
  });

  sdk.start();
};

3. Build and run your indexer to see the metrics and traces on your configured observability platform.

pnpm build
pnpm start --indexer=your-indexer

Sentry

1. Install required dependencies

First, install the required Sentry package:

pnpm install @sentry/node

2. Update instrumentation.ts to use Sentry

Update the instrumentation.ts file at the root of your project:

import * as Sentry from "@sentry/node";
import type { RegisterFn } from "apibara/types";

export const register: RegisterFn = async () => {
  Sentry.init({
    dsn: "__YOUR_DSN__",
    tracesSampleRate: 1.0,
  });
};

3. Build and run your indexer with Sentry error tracking enabled

pnpm build
pnpm start --indexer=your-indexer

The Sentry SDK uses OpenTelemetry under the hood, which means any OpenTelemetry instrumentation that emits spans will automatically be picked up by Sentry without additional configuration.

For more information on how to use Sentry with OpenTelemetry, refer to the Sentry documentation.


title: Top tips description: "Find most useful tips and patterns to help you get the most out of Apibara." updatedAt: 2025-09-23

Top tips

Find most useful tips and patterns to help you get the most out of Apibara.

General

Watching a file

You can watch files for changes during indexer execution, which is useful for development workflows and dynamic configuration updates. Here's an example of how to implement file watching using Node.js's built-in fs.watch:

import { watch } from "node:fs";
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer, reloadIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";
import type { ApibaraRuntimeConfig } from "apibara/types";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  return defineIndexer(StarknetStream)({
    streamUrl: "https://mainnet.starknet.a5a.ch",
    finality: "accepted",
    startingBlock: 10_000n,
    filter: {
      // ...
    },
    hooks: {
      "run:before": ({ abortSignal }) => {
        const logger = useLogger();
        logger.info("=== FILE WATCHER SET UP ===");

        watch("./tmp/test", { signal: abortSignal }, () => {
          logger.info("=== FILE CHANGED ===");
          reloadIndexer();
        });
      },
    },
    async transform({ endCursor, finality }) {
      // ...
    },
  });
}

⚠️ Important warnings:

  • Use watch instead of watchFile: When watching files, use fs.watch() instead of fs.watchFile(). The watch function works fine with reloadIndexer() or useIndexerContext(), but watchFile has compatibility issues with AsyncLocalStorage from node:async_hooks which is used internally by Apibara.

  • If you must use watchFile, make sure to call fs.unwatchFile() before setting up a new callback to prevent callback accumulation during indexer reloads and ensure latest context is used.

  • Multiple triggers per file change: Watch callbacks may be triggered multiple times for a single file change due to OS-level differences. Different operating systems handle file system events differently, so your callback might fire 2-3 times for one modification.

💡 Best practices:

  • Use the abortSignal parameter from hooks to properly clean up watchers when the indexer stops or reloads. This prevents orphaned watchers and ensures clean shutdown.
  • The abort signal is automatically triggered when the indexer is stopped or killed, making it perfect for cleanup scenarios during indexer reloads.

Reloading the indexer

You can programmatically reload your indexer using the reloadIndexer() function:

import { watch } from "node:fs";
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer, reloadIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";
import type { ApibaraRuntimeConfig } from "apibara/types";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  return defineIndexer(StarknetStream)({
    streamUrl: "https://mainnet.starknet.a5a.ch",
    finality: "accepted",
    startingBlock: 10_000n,
    filter: {
      // ...
    },
    async transform({ endCursor, finality }) {
      // ...
      if (endCursor?.orderKey === 150000n) {
        reloadIndexer();
      }
    },
  });
}

title: Upgrading from v1 description: "Learn how to upgrade your indexers to Apibara v2." diataxis: how-to updatedAt: 2025-06-11

Upgrading from v1

This guide explains how to upgrade your Starknet indexers from the old Apibara CLI experience to the new Apibara v2 experience.

At the time of writing (June 2025), Apibara v2 is the recommended version for new and existing applications.

Main changes

  • The underlying gRPC protocol and data types have changed. You can review all the changes on this page.
  • The old CLI has been replaced by a pure Typescript library. This means you can now leverage the full Node ecosystem (including Bun and Deno).
  • You can now extend indexers with plugins and hooks.

Migration

For this guide, we'll assume an indexer like the following:

export const config = {
  streamUrl: "https://mainnet.starknet.a5a.ch",
  startingBlock: 800_000,
  network: "starknet",
  finality: "DATA_STATUS_ACCEPTED",
  filter: {
    header: {},
  },
  sinkType: "console",
  sinkOptions: {},
};

export default function transform(block) {
  return block;
}

Step 1: initialize the Node project

Initialize the project to contain a package.json file:

npm init -y

Create the indexers/ folder where all the indexers will live:

mkdir indexers

Add the dependencies needed to run the indexer. If you're using any external dependencies, make sure to add them.

:::cli-command

npm add --save apibara@next @apibara/protocol@next @apibara/starknet@next
added 325 packages, and audited 327 packages in 11s

73 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

:::

Step 2: initialize the Apibara project

Create a new file called apibara.config.ts in the root of your project.

import { defineConfig } from "apibara/config";

export default defineConfig({});

Step 3: update the indexer

Now it's time to update the indexer.

  • Move the indexer to the indexers/ folder, ensuring that the file name ends with .indexer.ts.
  • Wrap the indexer in a defineIndexer(StarknetStream)({ /* ... */ }) call. Notice that now the stream configuration and transform function live in the same configuration object.
  • startingBlock is now a BigInt.
  • streamUrl is the same.
  • finality is now simpler to type.
  • The filter object changed. Please refer to the filter documentation for more information.
  • sinkType and sinkOptions are gone.
  • The transform function now takes named arguments, with block containing the block data.

The following git diff shows the changes to the indexer at the beginning of the guide.

diff --git a/simple.ts b/indexers/simple.indexer.ts
index bb09fdc..701a494 100644
--- a/simple.ts
+++ b/indexers/simple.indexer.ts
@@ -1,15 +1,18 @@
-export const config = {
-    streamUrl: "https://mainnet.starknet.a5a.ch",
-    startingBlock: 800_000,
-    network: "starknet",
-    finality: "DATA_STATUS_ACCEPTED",
+import { StarknetStream } from "@apibara/starknet";
+import { defineIndexer } from "apibara/indexer";
+import { useLogger } from "apibara/plugins";
+
+export default defineIndexer(StarknetStream)({
+    streamUrl: "https://mainnet.starknet.a5a.ch",
+    startingBlock: 800_000n,
+    finality: "accepted",
     filter: {
-        header: {},
+        header: "always",
     },
-    sinkType: "console",
-    sinkOptions: {},
-};
-
-export default function transform(block) {
-    return block;
-}
\ No newline at end of file
+    async transform({ block }) {
+        const logger = useLogger();
+        logger.info(block);
+    },
+});
\ No newline at end of file

Step 4: writing data

In version 1, the indexer would write data returned by transform to a sink. Now, you use plugins to write data to databases like PostgreSQL or MongoDB.

Refer to the plugins documentation for more information.

Sink-specific instructions

Webhook

Depending on your use-case, you have two strategies to update your existing webhook sink script to v2:

  • Call the external webhook using the fetch API.
  • Inline the webhook script in your indexer.

In the first case, transform the block's data like in V1 and then call the fetch method.

import { defineIndexer } from "apibara/indexer";
import { StarknetStream } from "@apibara/starknet";

import { transformBlock } from "../lib/helpers";

export default defineIndexer(StarknetStream)({
  streamUrl,
  finality: "accepted",
  startingBlock: 1_000_000n,
  filter: {
    events: [
      {
        address:
          "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
      },
    ],
  },
  plugins: [],
  async transform({ block }) {
    const payload = transformBlock(block);

    // Make an HTTP POST request to the webhook URL
    const response = await fetch("https://example.org/webhook", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    // Handle the response if needed.
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const result = await response.json();
  },
});

If you were using the "raw" mode for the webhook sink script, you also need to register a hook to call the webhook URL on invalidate messages.

import { defineIndexer } from "apibara/indexer";
import { StarknetStream } from "@apibara/starknet";

export default defineIndexer(StarknetStream)({
  /* same as before */
  async transform({ block }) {
    /* ... */
  },
  hooks: {
    async "message:invalidate"({ message }) {
      const { cursor } = message;
      const response = await fetch("https://example.org/webhook", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          // orderKey is a BigInt, so convert it to a string to safely send it to the webhook
          invalidate: {
            orderKey: String(cursor.orderKey),
            uniqueKey: cursor.uniqueKey,
          }
        }),
      });

      // Handle the response if needed.
    },
  },
});

Some users found that they can implement the webhook script inline in their indexer. This results in a more efficient indexer that is easier to maintain and deploy.

Please refer to the installation and getting started page for more information.


title: Drizzle with PostgreSQL description: "Store your indexer's data to PostgreSQL using Drizzle ORM." diataxis: reference updatedAt: 2025-05-01

Drizzle with PostgreSQL

The Apibara Indexer SDK supports Drizzle ORM for storing data to PostgreSQL.

Installation

Using the CLI

You can add an indexer that uses Drizzle for storage by selecting "PostgreSQL" in the "Storage" section when creating an indexer.

The CLI automatically updates your package.json to add all necessary dependencies.

Manually

To use Drizzle with PostgreSQL, you need to install the following dependencies:

npm install drizzle-orm pg @apibara/plugin-drizzle@next

We recommend using Drizzle Kit to manage the database schema.

npm install --save-dev drizzle-kit

Additionally, if you want to use PGLite to run a Postgres compatible database without a full Postgres installation, you should install that package too.

npm install @electric-sql/pglite

Persisting the indexer's state

The Drizzle plugin automatically persists the indexer's state to the database. You can explicitly configure this option with the persistState flag.

Read more about state persistence in the internals page.

Adding the plugin to your indexer

Add the drizzleStorage plugin to your indexer's plugins. Notice the following:

  • Use the drizzle helper exported by @apibara/plugin-drizzle to create a drizzle instance. This method supports creating an in-memory database (powered by PgLite) by specifying the memory: connection string.
  • Always specify the database schema. This schema is used by the indexer to know which tables it needs to protect against chain reorganizations.
  • By default, the connection string is read from the POSTGRES_CONNECTION_STRING environment variable. If left empty, a local PGLite database will be created. This is great because it means you don't need to start Postgres on your machine to develop locally!
import {
  drizzle,
  drizzleStorage,
  useDrizzleStorage,
} from "@apibara/plugin-drizzle";

import { transfers } from "@/lib/schema";

const db = drizzle({
  schema: {
    transfers,
  },
});

export default defineIndexer(EvmStream)({
  // ...
  plugins: [drizzleStorage({ db })],
  // ...
});

Schema configuration

You can use the pgTable function from drizzle-orm/pg-core to define the schema, no changes required.

The only important thing to notice is that your table must have an id column (name configurable) that uniquely identifies each row. This requirement is necessary to handle chain reorganizations. Read more how the plugin handles chain reorganizations on the internals page.

import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";

export const transfers = pgTable("transfers", {
  id: uuid("id").primaryKey().defaultRandom(),
  amount: bigint("amount", { mode: "number" }),
  transactionHash: text("transaction_hash"),
});

Specifying the id column

As mentioned in the previous section, the id column is required by the plugin to handle chain reorganizations.

The plugin allows you to specify the id column name for each table in the schema. You can do this by passing the idColumn option to the drizzleStorage plugin. This option accepts either a string value or a record mapping table names to column names. You can use the special "*" table name to define the default id column name for all tables.

Example

This example uses the same id column name (_id) for all tables.

export default defineIndexer(EvmStream)({
  // ...
  plugins: [
    drizzleStorage({
      db,
      idColumn: "_id",
    }),
  ],
  // ...
});

This example uses different id column names for each table. The transfers table will use transfer_id as the id column, while all other tables will use _id.

export default defineIndexer(EvmStream)({
  // ...
  plugins: [
    drizzleStorage({
      db,
      idColumn: {
        transfers: "transfer_id",
        "*": "_id",
      },
    }),
  ],
  // ...
});

Writing and reading data from within the indexer

Use the useDrizzleStorage hook to access the current database transaction. This transaction behaves exactly like a regular Drizzle ORM transaction because it is. Thanks to the way the plugin works and handles chain reorganizations, it can expose the full Drizzle ORM API without any limitations.

export default defineIndexer(EvmStream)({
  // ...
  async transform({ endCursor, block, context, finality }) {
    const { db } = useDrizzleStorage();

    for (const event of block.events) {
      await db.insert(transfers).values(decodeEvent(event));
    }
  },
});

You are not limited to inserting data, you can also update and delete rows.

Drizzle query

Using the Drizzle Query interface is easy. Pass the database instance to useDrizzleStorage: in this case the database type is used to automatically deduce the database schema.

Note: the database instance is not used to query data but only for type inference.

const database = drizzle({ schema, connectionString });

export default defineIndexer(EvmStream)({
  // ...
  async transform({ endCursor, block, context, finality }) {
    const { db } = useDrizzleStorage(database);

    const existingToken = await db.query.tokens.findFirst({ address });
  },
});

Querying data from outside the indexer

You can query data from your application like you always do, using the standard Drizzle ORM library.

Database migrations

There are two strategies you can adopt for database migrations:

  • run migrations separately, for example using the drizzle-kit CLI.
  • run migrations automatically upon starting the indexer.

If you decide to adopt the latter strategy, use the migrate option. Notice that the migrationsFolder path is relative from the project's root.

import { drizzle } from "@apibara/plugin-drizzle";

const database = drizzle({ schema });

export default defineIndexer(EvmStream)({
  // ...
  plugins: [
    drizzleStorage({
      db,
      migrate: {
        // Path relative to the project's root.
        migrationsFolder: "./migrations",
      },
    }),
  ],
  // ...
});

title: Testing description: "Learn how to test your indexer's when using the Drizzle plugin." diataxis: how-to updatedAt: 2025-09-12

Testing

The Drizzle plugin provides an in-memory database to simplify testing, powered by PGLite.

Indexer setup

Register the Drizzle plugin with your indexer. The default configuration automatically creates a PGLite database when running tests.

import { drizzleStorage, useDrizzleStorage } from "@apibara/plugin-drizzle";
import { drizzle } from "@apibara/plugin-drizzle";
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "apibara/indexer";

import { myTable } from "@/lib/schema";

export default function (runtimeConfig: ApibaraRuntimeConfig) {
  const database = drizzle({
    schema: {
      myTable,
    },
  });

  return defineIndexer(StarknetStream)({
    plugins: [
      drizzleStorage({
        db: database,
      }),
    ],
    async transform({ endCursor, block, context, finality }) {
      const { db } = useDrizzleStorage();

      // Do something with the database
    },
  });
}

Testing

The @apibara/plugin-drizzle package provides two helper functions to work with test databases:

  • useTestDrizzleStorage: get the Drizzle database object internally created by the plugin.
  • getTestDatabase: call it with the value returned by the vcr to get the Drizzle database after running the test.

If you need to initialize data in the database, you can add a hook to run:before and initialize the database there.

Example

The following example shows a complete end-te-end test for the indexer.

  • Pass a custom runtime configuration to the indexer's constructor.
  • Initialize the database before running the indexer.
  • Read the data from the database and assert its content with vitest snapshot matching.
import { describe, expect, it } from "vitest";
import { createVcr } from "apibara/testing";
import { useTestDrizzleStorage } from "@apibara/plugin-drizzle";
import { getTestDatabase } from "@apibara/plugin-drizzle/testing";

// Import the indexer's constructor
import createIndexer from "@/indexers/my-indexer.indexer";

const vcr = createVcr();

describe("my indexer", () => {
  it("should work", async () => {
    const indexer = createIndexer({
      /* runtime configuration */
    });

    const testResult = await vcr.run("ethereum-usdc-transfers", indexer, {
      range: {
        fromBlock: 10_000_000n,
        toBlock: 10_000_005n,
      },
      hooks: {
        "run:before": async () => {
          // Initialize the database
          const db = useTestDrizzleStorage();
          await db.insert(myTable).values({ /* ... */});

        },
      },
    });

    // Get the database created for this test.
    const database = getTestDatabase(testResult);

    // Use the database like any other Drizzle database object
    const rows = await database.select().from(myTable);

    expect(rows.map(({ _id, ...rest }) => rest)).toMatchInlineSnapshot(`
      /* ... */
    `);
  });
});

title: Drizzle's plugin internals description: "Store your indexer's data to PostgreSQL using Drizzle ORM." diataxis: reference updatedAt: 2025-03-30

Drizzle's plugin internals

This section describes how the Drizzle plugin works. Understanding the content of this page is not needed for using the plugin.

Drizzle and the indexer

The plugin wraps all database operations in the transform and factory functions in a database transaction. This ensures that the indexer's state is always consistent and that data is never lost due to crashes or network failures.

More specifically, the plugin is implemented as a middleware.

At a very high level, the plugin looks like the following:

indexer.hooks.hook("handler:middleware", async ({ use }) => {
  use(async (context, next) => {
    await db.transaction(async (txn) => {
      // Assign the transaction to the context, to be accessed using useDrizzleStorage
      context.db = txn;

      await next();

      delete context.db;

      // Update the indexer's state with cursor.
      await updateState(txn);
    });
  });
});

Chain reorganizations

The indexer needs to be able to rollback state after a chain reorganization. The behavior described in this section is only relevant for un-finalized blocks. Finalized blocks don't need special handling since they are, by definition, not going to be part of a chain reorganization.

The main idea is to create an "audit table" with all changes to the indexer's schema.

The name of the audit table is airfoil.reorg_rollback and has the following schema.

+------------+--------------+-----------------------+
| Column     | Type         | Modifiers             |
|------------+--------------+-----------------------|
| n          | integer      |  not null default ... |
| op         | character(1) |  not null             |
| table_name | text         |  not null             |
| cursor     | integer      |  not null             |
| row_id     | text         |                       |
| row_value  | jsonb        |                       |
| indexer_id | text         |  not null             |
+------------+--------------+-----------------------+

The data stored in the row_value column is specific to each operation (INSERT, DELETE, UPDATE) contains the data needed to revert the operation. Notice that the table's row must be JSON-serializable.

At each block, the plugin registers a trigger for each table managed by the indexer. At the end of the transaction, the trigger inserts data into the audit table.

The audit table is periodically pruned to remove snapshots of data that is now finalized.

Reverting a block

When a chain reorganization is detected, all operations in the audit table where cursor is greater than the new chain's head are reverted in reverse order.

  • op = INSERT: the row with id row_id is deleted from the table.
  • op = DELETE: the row with id row_id is inserted back into the table, with the value stored in row_value.
  • op = UPDATE: the row with id row_id is updated in the table, with the value stored in row_value.

State persistence

The state of the indexer is persisted in the database, in the airfoil.checkpoints and airfoil.filters tables.

The checkpoints table contains the last indexed block for each indexer.

+------------+--------------+-----------------------+
| Column     | Type         | Modifiers             |
|------------+--------------+-----------------------|
| id         | text         |  primary key          |
| order_key  | integer      |  not null             |
| unique_key | text         |                       |
+------------+--------------+-----------------------+

The filters table is used to manage the dynamic filter of factory indexers. It contains the JSON-serialized filter together with the block range it applies to.

+------------+--------------+-----------------------+
| Column     | Type         | Modifiers             |
|------------+--------------+-----------------------|
| id         | text         |  not null             |
| filter     | text         |  not null             |
| from_block | integer      |  not null             |
| to_block   | integer      |  default null         |
+------------+--------------+-----------------------+

title: Drizzle's plugin API reference description: "Learn all available options and functions in the Drizzle's plugin." diataxis: reference updatedAt: 2025-11-19

Drizzle's plugin API reference

drizzle

This helper is used to create a Drizzle database that integrates with the Apibara indexer and cli.

import { drizzle } from "@apibara/plugin-drizzle";

// Here `transfers` and `balances` are standard Drizzle tables.
const database = drizzle({
  schema: {
    transfers,
    balances,
  }
});

Arguments

schema: TSchema

A standard Drizzle schema, that is a map of tables.

connectionString: string

The connection string. By default, the helper uses the value of the POSTGRES_CONNECTION_STRING environment variable. If the connection string is memory://, the indexer will run with an in-memory PgLite database.

drizzleStorage

This function is used to create the Drizzle's plugin. This is how you configure its behaviour.

import { drizzleStorage } from "@apibara/plugin-drizzle";

export default defineIndexer(...)({
  plugins: [
    drizzleStorage({
      db: database,
      migrate: {
        migrationsFolder: "./drizzle",
      },
    }),
  ]
})

Arguments

db: PgDatabase

The Drizzle database instance.

persistState: boolean

Whether to persist the indexer's state. Defaults to true.

indexerName: string

The name of the indexer. The default is the filename of the indexer.

schema: TSchema

The Drizzle schema containing the tables used by indexers.

idColumn: string | IdColumnMap

The column to use as the row id. Defaults to id.

If your tables use different names for the id columns, you can pass a record to this argument. For example, the following snippets uses the address column for the balances table, and id for all other tables (specified using the special * name):

drizzleStorage({
  idColumn: {
    "balances": "address",
    "*": "id"
  }
})

migrate: MigrateConfig

The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.

recordChainReorganizations: boolean

Whether to record chain reorganizations in the database. Defaults to false.

useDrizzleStorage

This hook returns the current Postgres transaction and is the only way to access the database from within the indexer.

import { useDrizzleStorage } from "@apibara/plugin-drizzle";

export default defineIndexer(...)({
  async transform({ ... }) {
    const { db } = useDrizzleStorage();

    await db.insert(...);
  }
})

title: Drizzle's plugin - Frequently Asked Questions description: "Find answers to common questions about using Drizzle with PostgreSQL." updatedAt: 2025-03-18

Frequently Asked Questions

General

Argument of type PgTableWithColumns is not assignable to parameter of type PgTable.

When structuring your project as monorepo, you may encounter the following error when type checking your project.

indexers/my-indexer.indexer.ts:172:25 - error TS2345: Argument of type 'PgTableWithColumns<{ name: "my_table"; schema: undefined; columns: { ... }; dialect:...' is not assignable to parameter of type 'PgTable<TableConfig>'.
  The types of '_.config.columns' are incompatible between these types.
    Type '{ ... }' is not assignable to type 'Record<string, PgColumn<ColumnBaseConfig<ColumnDataType, string>, {}, {}>>'.
      Property 'myColumn' is incompatible with index signature.
        Type 'PgColumn<{ name: "my_column"; tableName: "my_table"; dataType: "number"; columnType: "PgBigInt53"; data: number; driverParam: string | number; notNull: false; hasDefault: false; isPrimaryKey: false; ... 5 more ...; generated: undefined; }, {}, {}>' is not assignable to type 'PgColumn<ColumnBaseConfig<ColumnDataType, string>, {}, {}>'.
          The types of 'table._.config.columns' are incompatible between these types.
            Type 'Record<string, import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.10_pg@8.14.1/node_modules/drizzle-orm/pg-core/columns/common").PgColumn<import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.10_pg@8.14.1/node_modules/...' is not assignable to type 'Record<string, import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.11_pg@8.14.1/node_modules/drizzle-orm/pg-core/columns/common").PgColumn<import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.11_pg@8.14.1/node_modules/...'.
              'string' index signatures are incompatible.
                Type 'import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.10_pg@8.14.1/node_modules/drizzle-orm/pg-core/columns/common").PgColumn<import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.10_pg@8.14.1/node_modules/drizzle-orm/col...' is not assignable to type 'import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.11_pg@8.14.1/node_modules/drizzle-orm/pg-core/columns/common").PgColumn<import("/my/project/node_modules/.pnpm/drizzle-orm@0.40.1_@types+pg@8.11.11_pg@8.14.1/node_modules/drizzle-orm/col...'.
                  Property 'config' is protected but type 'Column<T, TRuntimeConfig, TTypeConfig>' is not a class derived from 'Column<T, TRuntimeConfig, TTypeConfig>'.

            await db.insert(myTable).values(rows);

This error is caused by different versions of drizzle-orm, pg, and @types/pg being used in different packages in your project. The solution is to make sure all of them use the same version, delete the node_modules folder and reinstall your dependencies.

Cancelling statement due to statement timeout

When running the indexer, it hangs due to a statement timeout. The error looks like this:

[error] Failed to run handler:middleware
  at .apibara/build/start.mjs:48165:12
  at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
  at async dispatch (.apibara/build/start.mjs:32196:4)
  at async .apibara/build/start.mjs:32982:5
  at async dispatch (.apibara/build/start.mjs:32196:4)
  at async _composedIndexerMiddleware (.apibara/build/start.mjs:32427:3)
  at async .apibara/build/start.mjs:32317:7
  at async .apibara/build/start.mjs:32310:6
  at async Object.callAsync (.apibara/build/start.mjs:30357:12)
  at async run (.apibara/build/start.mjs:32270:2)

  [cause]: canceling statement due to statement timeout
    at node_modules/.pnpm/pg-pool@3.10.1_pg@8.16.3/node_modules/pg-pool/index.js:45:11
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async Object.transform (.apibara/build/start.mjs:79558:6)
    at async .apibara/build/start.mjs:32352:9
    at async .apibara/build/start.mjs:32351:19
    at async dispatch (.apibara/build/start.mjs:32190:15)
    at async .apibara/build/start.mjs:48153:7
    at async .apibara/build/start.mjs:47632:10
    at async NodePgSession.transaction (.apibara/build/start.mjs:47307:20)
    at async withTransaction (.apibara/build/start.mjs:47631:9)

[error] Failed to run handler:middleware

This happens because internally the Drizzle plugin creates a transaction for each block to ensure data consistency. This means you cannot use the root Drizzle database object directly because it will hang indefinitely while waiting for the transaction to complete.

Solution: Use the database object returned by the useDrizzleStorage() hook. This is the current database transaction.

import { useDrizzleStorage } from "@apibara/plugin-drizzle";

export default defineIndexer(StarknetStream)({
  // ...
  async transform({ endCursor, block, context, finality }) {
    // Use this database object.
    // This object provides all the methods available in the root Drizzle
    // database object, but it's a transaction-specific database object.
    const { db } = useDrizzleStorage();
  },
});

Performance

Why is indexing slower after I add the plugin?

There are many possible reasons for this, but the most common ones are:

  • The latency between your indexer and the database is high.
  • Your indexer is inserting rows too frequently.

In the first case, consider moving your indexer's deployment closer to the database to improve latency.

In the second case, consider using a bulk insert strategy to reduce the number of individual insert operations. Usually, this means converting many db.insert(..) operations inside a loop into a single db.insert() call.

// Before
for (const event of block.events) {
  const transfer = decodeEvent(event);
  await db.insert(schema.transfers).values(transfer);
}

// After
const transfers = block.events.map((event) => decodeEvent(event));
await db.insert(schema.transfers).values(transfers);

title: Starknet description: "Stream Starknet data with Apibara." diataxis: reference updatedAt: 2024-10-22

Starknet


                                                   ,//(//,.
                     (@.                    .#@@@@@@@@@@@@@@@@@#.
                    (@@&.                (@@@@@@@@@@@@@@@@@@@@@@@@&.
                /%@@@@@@@@@%.         ,@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@,
                    .@@#            *@@@@@@@@@@@@@@@@@@@@@@@&&&&&&&@@@%
                     *%           ,@@@@@@@@@@@@@@@@@@@@#((((((((((((,
                                .@@@@@@@@@@@@@@@@@@@%((((((((((((,
                              .@@@@@@@@@@@@@@@@@@@&((((((((((((.
                            .@@@@@@@@@@@@@@@@@@@@%(((((((((((.
                          *@@@@@@@@@@@@@@@@@@@@&(((((((((((,
                      .%@@@@@@@@@@@@@@@@@@@@@@#((((((((((*
        @@@@@&&&@@@@@@@@@@@@@@@@@@@@@@@@@@@@#((((((((((/
         %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#(((((((((((.
          ,&@@@@@@@@@@@@@@@@@@@@@@@@@@@#((((((((((((.
            ,#@@@@@@@@@@@@@@@@@@@@@&#((((((((((((*        .,**.
              ./(#%@@@@@@@@@@@&#((((((((((((((/         *(((((((/
                  *(((((((((((((((((((((((/.            (((((((((
                      .*/(((((((((((/*.                  /((((((.



Apibara provides data streams for all Starknet networks. Notice that these stream URLs are going to change in the future when DNA v2 is released.

Starknet Mainnet

https://mainnet.starknet.a5a.ch

Starknet Sepolia

https://sepolia.starknet.a5a.ch

Starknet appchains

You can ingest data from Starknet appchains and serve it using our open-source DNA service. Please get in touch with the team if you'd like a managed solution.


title: Starknet filter reference description: "Starknet: DNA data filter reference guide." diataxis: reference updatedAt: 2025-04-17

Starknet filter reference

This page contains reference about the available data filters for Starknet DNA streams.

Related pages

Filter ID

All filters have an associated ID. When the server filters a block, it will return a list of all filters that matched a piece of data with the data. You can use this ID to build powerful abstractions in your indexers.

Field elements

Apibara represents Starknet field elements as hex-encode strings.

export type FieldElement = `0x${string}`;

Filter types

Root

The root filter object contains a collection of filters. Notice that providing an empty filter object is an error.

export type Filter = {
  header?: HeaderFilter;
  transactions?: TransactionFilter[];
  events?: EventFilter[];
  messages?: MessageToL1Filter[];
  storageDiffs?: StorageDiffFilter[];
  contractChanges?: ContractChangeFilter[];
  nonceUpdates?: NonceUpdateFilter[];
};

Header

The HeaderFilter object controls when the block header is returned to the client.

export type HeaderFilter = "always" | "on_data" | "on_data_or_on_new_block";

The values have the following meaning:

  • always: Always return the header, even if no other filter matches.
  • on_data: Return the header only if any other filter matches. This is the default value.
  • on_data_or_on_new_block: Return the header only if any other filter matches. If no other filter matches, return the header only if the block is a new block.

Events

Events are the most common filter used by Apibara users. You can filter by smart contract or event selector.

export type EventFilter = {
  id?: number;
  address?: FieldElement;
  keys?: (FieldElement | null)[];
  strict?: boolean;
  transactionStatus?: "succeeded" | "reverted" | "all";
  includeTransaction?: boolean;
  includeReceipt?: boolean;
  includeMessages?: boolean;
  includeSiblings?: boolean;
};

Properties

  • address: filter by contract address. If empty, matches any contract address.
  • keys: filter by keys. Use null to match any value. The server will filter based only the first four elements of the array.
  • strict: return events whose keys length matches the filter. By default, the filter does a prefix match on the keys.
  • transactionStatus: return events emitted by transactions with the provided status. Defaults to succeeded.
  • includeTransaction: also return the transaction that emitted the event.
  • includeReceipt: also return the receipt of the transaction that emitted the event.
  • includeMessages: also return all messages to L1 sent by the transaction that emitted the event.
  • includeSiblings: also return all other events emitted by the same transaction that emitted the matched event.

Examples

  • All events from a specific smart contract.
const filter = {
  events: [{ address: MY_CONTRACT }],
};
  • Multiple events from the same smart contract.
const filter = {
  events: [
    {
      address: MY_CONTRACT,
      keys: [getSelector("Approve")],
    },
    {
      address: MY_CONTRACT,
      keys: [getSelector("Transfer")],
    },
  ],
};
  • Multiple events from different smart contracts.
const filter = {
  events: [
    {
      address: CONTRACT_A,
      keys: [getSelector("Transfer")],
    },
    {
      address: CONTRACT_B,
      keys: [getSelector("Transfer")],
      includeReceipt: false,
    },
    {
      address: CONTRACT_C,
      keys: [getSelector("Transfer")],
    },
  ],
};
  • All Transfer events, from any contract.
const filter = {
  events: [
    {
      keys: [getSelector("Transfer")],
    },
  ],
};
  • All "new type" Transfer events with indexed sender and destination addresses.
const filter = {
  events: [
    {
      keys: [getSelector("Transfer"), null, null],
      strict: true,
    },
  ],
};

Transactions

Transactions on Starknet can be of different type (invoke, declare contract, deploy contract or account, handle L1 message). Clients can request all transactions or filter by transaction type.

export type InvokeTransactionV0Filter = {
  _tag: "invokeV0";
  invokeV0: {};
};

export type InvokeTransactionV1Filter = {
  _tag: "invokeV1";
  invokeV1: {};
};

export type InvokeTransactionV3Filter = {
  _tag: "invokeV3";
  invokeV3: {};
};

export type DeployTransactionFilter = {
  _tag: "deploy";
  deploy: {};
};

export type DeclareV0TransactionFilter = {
  _tag: "declareV0";
  declareV0: {};
};

export type DeclareV1TransactionFilter = {
  _tag: "declareV1";
  declareV1: {};
};

export type DeclareV2TransactionFilter = {
  _tag: "declareV2";
  declareV2: {};
};

export type DeclareV3TransactionFilter = {
  _tag: "declareV3";
  declareV3: {};
};

export type L1HandlerTransactionFilter = {
  _tag: "l1Handler";
  l1Handler: {};
};

export type DeployAccountV1TransactionFilter = {
  _tag: "deployAccountV1";
  deployAccountV1: {};
};

export type DeployAccountV3TransactionFilter = {
  _tag: "deployAccountV3";
  deployAccountV3: {};
};

export type TransactionFilter = {
  id?: number;
  transactionStatus?: "succeeded" | "reverted" | "all";
  includeReceipt?: boolean;
  includeMessages?: boolean;
  includeEvents?: boolean;
  transactionType?:
    | InvokeTransactionV0Filter
    | InvokeTransactionV1Filter
    | InvokeTransactionV3Filter
    | DeployTransactionFilter
    | DeclareV0TransactionFilter
    | DeclareV1TransactionFilter
    | DeclareV2TransactionFilter
    | DeclareV3TransactionFilter
    | L1HandlerTransactionFilter
    | DeployAccountV1TransactionFilter
    | DeployAccountV3TransactionFilter;
};

Properties

  • transactionStatus: return transactions with the provided status. Defaults to succeeded.
  • includeReceipt: also return the receipt of the transaction.
  • includeMessages: also return the messages to L1 sent by the transaction.
  • includeEvents: also return the events emitted by the transaction.
  • transactionType: filter by transaction type.

Examples

  • Request all transactions in a block. Notice the empty transaction filter object, this filter will match any transaction.
const filter = { transactions: [{}] };
  • Request all transactions of a specific type, e.g. deploy account. In this case we specify the deployAccountV3 variant.
const filter = {
  transactions: [
    {
      transactionType: { _tag: "deployAccountV3", deployAccountV3: {} },
    },
  ],
};

Messages

Filter messages from L1 to Starknet.

export type MessageToL1Filter = {
  id?: number;
  fromAddress?: FieldElement;
  toAddress?: FieldElement;
  transactionStatus?: "succeeded" | "reverted" | "all";
  includeTransaction?: boolean;
  includeReceipt?: boolean;
  includeEvents?: boolean;
};

Properties

  • fromAddress: filter by sender address. If empty, matches any sender address.
  • toAddress: filter by receiver address. If empty, matches any receiver address.
  • transactionStatus: return messages with the provided status. Defaults to succeeded.
  • includeTransaction: also return the transaction that sent the message.
  • includeReceipt: also return the receipt of the transaction that sent the message.
  • includeEvents: also return the events emitted by the transaction that sent the message.

Storage diff

Request changes to the storage of one or more contracts.

export type StorageDiffFilter = {
  id?: number;
  contractAddress?: FieldElement;
};

Properties

  • contractAddress: filter by contract address. If empty, matches any contract address.

Contract change

Request changes to the declared or deployed contracts.

export type DeclaredClassFilter = {
  _tag: "declaredClass";
  declaredClass: {};
};

export type ReplacedClassFilter = {
  _tag: "replacedClass";
  replacedClass: {};
};

export type DeployedContractFilter = {
  _tag: "deployedContract";
  deployedContract: {};
};

export type ContractChangeFilter = {
  id?: number;
  change?: DeclaredClassFilter | ReplacedClassFilter | DeployedContractFilter;
};

Properties

  • change: filter by change type.
    • declaredClass: receive declared classes.
    • replacedClass: receive replaced classes.
    • deployedContract: receive deployed contracts.

Nonce update

Request changes to the nonce of one or more contracts.

export type NonceUpdateFilter = {
  id?: number;
  contractAddress?: FieldElement;
};

Properties

  • contractAddress: filter by contract address. If empty, matches any contract.

title: Starknet data reference description: "Starknet: DNA data data reference guide." diataxis: reference updatedAt: 2025-09-03

Starknet data reference

This page contains reference about the available data in Starknet DNA streams.

Related pages

Filter ID

All filters have an associated ID. To help clients correlate filters with data, the filter ID is included in the filterIds field of all data objects. This field contains the list of all filter IDs that matched a piece of data.

Nullable fields

Important: most fields are nullable to allow evolving the protocol. You should always assert the presence of a field for critical indexers.

Field elements

Apibara represents Starknet field elements as hex-encode strings.

export type FieldElement = `0x${string}`;

Data types

Block

The root object is the Block.

export type Block = {
  header?: BlockHeader;
  transactions: Transaction[];
  receipts: TransactionReceipt[];
  events: Event[];
  messages: MessageToL1[];
  storageDiffs: StorageDiff[];
  contractChanges: ContractChange[];
  nonceUpdates: NonceUpdate[];
};

Block header

This is the block header, which contains information about the block.

export type BlockHeader = {
  blockHash?: FieldElement;
  parentBlockHash?: FieldElement;
  blockNumber?: bigint;
  sequencerAddress?: FieldElement;
  newRoot?: FieldElement;
  timestamp?: Date;
  starknetVersion?: string;
  l1GasPrice?: ResourcePrice;
  l1DataGasPrice?: ResourcePrice;
  l1DataAvailabilityMode?: "blob" | "calldata";
};

export type ResourcePrice = {
  priceInFri?: FieldElement;
  priceInWei?: FieldElement;
};

Properties

  • blockHash: the block hash.

  • parentBlockHash: the block hash of the parent block.

  • blockNumber: the block number.

  • sequencerAddress: the sequencer address.

  • newRoot: the new state root.

  • timestamp: the block timestamp.

  • starknetVersion: the Starknet version.

  • l1GasPrice: the L1 gas price.

  • l1DataGasPrice: the L1 data gas price.

  • l1DataAvailabilityMode: the L1 data availability mode.

  • priceInFri: the price of L1 gas in the block, in units of fri (10^-18 $STRK).

  • priceInWei: the price of L1 gas in the block, in units of wei (10^-18 $ETH).

Event

An event is emitted by a transaction.

export type Event = {
  filterIds: number[];
  address?: FieldElement;
  keys: FieldElement[];
  data: FieldElement[];
  eventIndex?: number;
  transactionIndex?: number;
  transactionHash?: FieldElement;
  transactionStatus?: "succeeded" | "reverted";
};

Properties

  • address: the address of the contract that emitted the event.
  • keys: the keys of the event.
  • data: the data of the event.
  • eventIndex: the index of the event in the block.
  • transactionIndex: the index of the transaction that emitted the event.
  • transactionHash: the hash of the transaction that emitted the event.
  • transactionStatus: the status of the transaction that emitted the event.

Relevant filters

  • filter.events
  • filter.transactions[].includeEvents
  • filter.events[].includeSiblings
  • filter.messages[].includeEvents

Transaction

Starknet has different types of transactions, all of them are grouped together in the Transaction type. Common transaction information is accessible in the meta field.

export type TransactionMeta = {
  transactionIndex?: number;
  transactionHash?: FieldElement;
  transactionStatus?: "succeeded" | "reverted";
};

export type Transaction = {
  filterIds: number[];
  meta?: TransactionMeta;
  transaction?:
    | InvokeTransactionV0
    | InvokeTransactionV1
    | InvokeTransactionV3
    | L1HandlerTransaction
    | DeployTransaction
    | DeclareTransactionV0
    | DeclareTransactionV1
    | DeclareTransactionV2
    | DeclareTransactionV3
    | DeployAccountTransactionV1
    | DeployAccountTransactionV3;
};

Properties

  • meta: transaction metadata.

  • transaction: the transaction type.

  • meta.transactionIndex: the index of the transaction in the block.

  • meta.transactionHash: the hash of the transaction.

  • meta.transactionStatus: the status of the transaction.

Relevant filters

  • filter.transactions
  • filter.events[].includeTransaction
  • filter.messages[].includeTransaction
export type InvokeTransactionV0 = {
  _tag: "invokeV0";
  invokeV0: {
    maxFee?: FieldElement;
    signature: FieldElement[];
    contractAddress?: FieldElement;
    entryPointSelector?: FieldElement;
    calldata: FieldElement[];
  };
};

Properties

  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • contractAddress: the address of the contract that will receive the call.
  • entryPointSelector: the selector of the function that will be called.
  • calldata: the calldata of the transaction.
export type InvokeTransactionV1 = {
  _tag: "invokeV1";
  invokeV1: {
    senderAddress?: FieldElement;
    calldata: FieldElement[];
    maxFee?: FieldElement;
    signature: FieldElement[];
    nonce?: FieldElement;
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • calldata: the calldata of the transaction.
  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • nonce: the nonce of the transaction.
export type ResourceBounds = {
  maxAmount?: bigint;
  maxPricePerUnit?: bigint;
};

export type ResourceBoundsMapping = {
  l1Gas?: ResourceBounds;
  l2Gas?: ResourceBounds;
};

export type InvokeTransactionV3 = {
  _tag: "invokeV3";
  invokeV3: {
    senderAddress?: FieldElement;
    calldata: FieldElement[];
    signature: FieldElement[];
    nonce?: FieldElement;
    resourceBounds?: ResourceBoundsMapping;
    tip?: bigint;
    paymasterData: FieldElement[];
    accountDeploymentData: FieldElement[];
    nonceDataAvailabilityMode?: "l1" | "l2";
    feeDataAvailabilityMode?: "l1" | "l2";
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • calldata: the calldata of the transaction.
  • signature: the signature of the transaction.
  • nonce: the nonce of the transaction.
  • resourceBounds: the resource bounds of the transaction.
  • tip: the tip of the transaction.
  • paymasterData: the paymaster data of the transaction.
  • accountDeploymentData: the account deployment data of the transaction.
  • nonceDataAvailabilityMode: the nonce data availability mode of the transaction.
  • feeDataAvailabilityMode: the fee data availability mode of the transaction.
export type L1HandlerTransaction = {
  _tag: "l1Handler";
  l1Handler: {
    contractAddress?: FieldElement;
    entryPointSelector?: FieldElement;
    calldata: FieldElement[];
    nonce?: bigint;
  };
};

Properties

  • contractAddress: the address of the contract that will receive the call.
  • entryPointSelector: the selector of the function that will be called.
  • calldata: the calldata of the transaction.
  • nonce: the nonce of the transaction.
export type DeployTransaction = {
  _tag: "deploy";
  deploy: {
    contractAddressSalt?: FieldElement;
    constructorCalldata: FieldElement[];
    classHash?: FieldElement;
  };
};

Properties

  • contractAddressSalt: the salt used to compute the contract address.
  • constructorCalldata: the calldata used to initialize the contract.
  • classHash: the class hash of the contract.
export type DeclareTransactionV0 = {
  _tag: "declareV0";
  declareV0: {
    senderAddress?: FieldElement;
    maxFee?: FieldElement;
    signature: FieldElement[];
    classHash?: FieldElement;
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • classHash: the class hash of the contract.
export type DeclareTransactionV1 = {
  _tag: "declareV1";
  declareV1: {
    senderAddress?: FieldElement;
    maxFee?: FieldElement;
    signature: FieldElement[];
    classHash?: FieldElement;
    nonce?: FieldElement;
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • classHash: the class hash of the contract.
  • nonce: the nonce of the transaction.
export type DeclareTransactionV2 = {
  _tag: "declareV2";
  declareV2: {
    senderAddress?: FieldElement;
    maxFee?: FieldElement;
    signature: FieldElement[];
    classHash?: FieldElement;
    nonce?: FieldElement;
    compiledClassHash?: FieldElement;
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • classHash: the class hash of the contract.
  • nonce: the nonce of the transaction.
  • compiledClassHash: the compiled class hash of the contract.
export type ResourceBounds = {
  maxAmount?: bigint;
  maxPricePerUnit?: bigint;
};

export type ResourceBoundsMapping = {
  l1Gas?: ResourceBounds;
  l2Gas?: ResourceBounds;
};

export type DeclareTransactionV3 = {
  _tag: "declareV3";
  declareV3: {
    senderAddress?: FieldElement;
    maxFee?: FieldElement;
    signature: FieldElement[];
    classHash?: FieldElement;
    nonce?: FieldElement;
    compiledClassHash?: FieldElement;
    resourceBounds?: ResourceBoundsMapping;
    tip?: bigint;
    paymasterData: FieldElement[];
    accountDeploymentData: FieldElement[];
    nonceDataAvailabilityMode?: "l1" | "l2";
    feeDataAvailabilityMode?: "l1" | "l2";
  };
};

Properties

  • senderAddress: the address of the account that will send the transaction.
  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • classHash: the class hash of the contract.
  • nonce: the nonce of the transaction.
  • compiledClassHash: the compiled class hash of the contract.
  • resourceBounds: the resource bounds of the transaction.
  • tip: the tip of the transaction.
  • paymasterData: the paymaster data of the transaction.
  • accountDeploymentData: the account deployment data of the transaction.
  • nonceDataAvailabilityMode: the nonce data availability mode of the transaction.
  • feeDataAvailabilityMode: the fee data availability mode of the transaction.
export type DeployAccountTransactionV1 = {
  _tag: "deployAccountV1";
  deployAccountV1: {
    maxFee?: FieldElement;
    signature: FieldElement[];
    nonce?: FieldElement;
    contractAddressSalt?: FieldElement;
    constructorCalldata: FieldElement[];
    classHash?: FieldElement;
  };
};

Properties

  • maxFee: the maximum fee for the transaction.
  • signature: the signature of the transaction.
  • nonce: the nonce of the transaction.
  • contractAddressSalt: the salt used to compute the contract address.
  • constructorCalldata: the calldata used to initialize the contract.
  • classHash: the class hash of the contract.
export type DeployAccountTransactionV3 = {
  _tag: "deployAccountV3";
  deployAccountV3: {
    signature: FieldElement[];
    nonce?: FieldElement;
    contractAddressSalt?: FieldElement;
    constructorCalldata: FieldElement[];
    n;
    classHash?: FieldElement;
    resourceBounds?: ResourceBoundsMapping;
    tip?: bigint;
    paymasterData: FieldElement[];
    nonceDataAvailabilityMode?: "l1" | "l2";
    feeDataAvailabilityMode?: "l1" | "l2";
  };
};

Properties

  • signature: the signature of the transaction.
  • nonce: the nonce of the transaction.
  • contractAddressSalt: the salt used to compute the contract address.
  • constructorCalldata: the calldata used to initialize the contract.
  • classHash: the class hash of the contract
  • resourceBounds: the resource bounds of the transaction.
  • tip: the tip of the transaction.
  • paymasterData: the paymaster data of the transaction.
  • nonceDataAvailabilityMode: the nonce data availability mode of the transaction.
  • feeDataAvailabilityMode: the fee data availability mode of the transaction.

Transaction receipt

The receipt of a transaction contains information about the execution of the transaction.

export type TransactionReceipt = {
  filterIds: number[];
  meta?: TransactionReceiptMeta;
  receipt?:
    | InvokeTransactionReceipt
    | L1HandlerTransactionReceipt
    | DeclareTransactionReceipt
    | DeployTransactionReceipt
    | DeployAccountTransactionReceipt;
};

export type TransactionReceiptMeta = {
  transactionIndex?: number;
  transactionHash?: FieldElement;
  actualFee?: FeePayment;
  executionResources?: ExecutionResources;
  executionResult?: ExecutionSucceeded | ExecutionReverted;
};

export type InvokeTransactionReceipt = {
  _tag: "invoke";
  invoke: {};
};

export type L1HandlerTransactionReceipt = {
  _tag: "l1Handler";
  l1Handler: {
    messageHash?: Uint8Array;
  };
};

export type DeclareTransactionReceipt = {
  _tag: "declare";
  declare: {};
};

export type DeployTransactionReceipt = {
  _tag: "deploy";
  deploy: {
    contractAddress?: FieldElement;
  };
};

export type DeployAccountTransactionReceipt = {
  _tag: "deployAccount";
  deployAccount: {
    contractAddress?: FieldElement;
  };
};

export type ExecutionSucceeded = {
  _tag: "succeeded";
  succeeded: {};
};

export type ExecutionReverted = {
  _tag: "reverted";
  reverted: {
    reason: string;
  };
};

export type FeePayment = {
  amount?: FieldElement;
  unit?: "wei" | "strk";
};

Relevant filters

  • filter.transactions[].includeReceipt
  • filter.events[].includeReceipt
  • filter.messages[].includeReceipt

Message to L1

A message to L1 is sent by a transaction.

export type MessageToL1 = {
  filterIds: number[];
  fromAddress?: FieldElement;
  toAddress?: FieldElement;
  payload: FieldElement[];
  messageIndex?: number;
  transactionIndex?: number;
  transactionHash?: FieldElement;
  transactionStatus?: "succeeded" | "reverted";
};

Properties

  • fromAddress: the address of the contract that sent the message.
  • toAddress: the address of the contract that received the message.
  • payload: the payload of the message.
  • messageIndex: the index of the message in the block.
  • transactionIndex: the index of the transaction that sent the message.
  • transactionHash: the hash of the transaction that sent the message.
  • transactionStatus: the status of the transaction that sent the

… [truncated — open the raw llms.txt above for the full file]

Related

The AI Toolkit for TypeScript, from the creators of Next.js.

/llms.txt
136,985 tokens
Developer Tools

Meet the modern standard for public facing documentation. Beautiful out of the box, easy to maintain, and optimized for user engagement.

/llms.txt
5,436 tokens
/llms-full.txt
181,290 tokens
Developer Tools

Web development for the rest of us.

/llms.txt
602 tokens
/llms-full.txt
453,623 tokens
Developer Tools

Search through billions of items for similar matches to any object, in milliseconds. It’s the next generation of search, an API call away.

/llms.txt
15,715 tokens
/llms-full.txt
588,629 tokens
Developer Tools

Build and deploy reliable background jobs with no timeouts and no infrastructure to manage.

/llms.txt
12,202 tokens
/llms-full.txt
387,586 tokens
Developer Tools

Get the simple developer experience of SQLite in production, and scale your multi-tenant backend with unlimited databases.

/llms.txt
10,006 tokens
/llms-full.txt
163,317 tokens
Developer Tools

Upstash is a serverless data platform providing low latency and high scalability for real-time applications.

/llms.txt
52,307 tokens
/llms-full.txt
1,200,134 tokens
Developer Tools

One-click deployments built for teams, tuned for Laravel, loaded with tools and goodies you're going to love.

/llms.txt
565 tokens
/llms-full.txt
11,330 tokens
Developer Tools