# Resume with cursors

Source: https://docs.graphops.xyz/substreams/guides/resume-with-cursors

Restart a consumer after a disconnect or deploy without losing or duplicating data. Save each
block's cursor in the same transaction as its output, then reconnect with the latest committed
cursor. The stream continues at the next block.

_Rows and cursor commit in one transaction. After a restart, the consumer reconnects with the stored cursor and continues at the next block without skipping any._

1. `BlockScopedData` arrives with block N and cursor cN.
2. One transaction: rows (output for block N) and cursor (cN).
3. Restart: Resume from cN, continuing at block N+1.

## Prerequisites

* A durable database, checkpoint table, or state store.
* A consumer that receives `BlockScopedData` messages and their cursors.
* Idempotent output writes.

## Commit, then resume

For each `BlockScopedData` message:

1. Write the block's output.
2. Save the message's cursor in the same transaction.
3. Commit, then process the next message.

When the consumer restarts, read the latest committed cursor and pass it to the stream. The CLI
tab resumes with `--cursor`. The complete Go, Rust, and TypeScript consumers resume from `GRAPHOPS_CURSOR`. Set it to your last committed cursor. They print each decoded output and its cursor; connect that handoff to your database transaction before using the consumer in production.

CLI · Substreams CLI

Prerequisites

1. Install the Substreams CLI.

2. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

3. Replace `<LAST_COMMITTED_CURSOR>` with the cursor of the last block you committed.

Tested with ethereum-common v0.4.0, substreams 1.25.0.

[Example project](/examples/substreams.guides.resume-with-cursors.sh-1.tar.gz)

```sh
# The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header.
export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}"

# --cursor resumes right after the block where that cursor was committed.
substreams run \
  -e "https://1.evm.substreams.graphops.xyz" \
  "https://spkg.io/v1/packages/ethereum-common/v0.4.0" \
  "filtered_events" \
  --params "filtered_events=evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" \
  --start-block 18000002 \
  --stop-block 18000008 \
  --cursor "<LAST_COMMITTED_CURSOR>" \
  -o jsonl

```

Output

```json
{
  "@module": "filtered_events",
  "@block": 18000003,
  "@type": "sf.substreams.ethereum.v1.Events",
  "@data": {
    "clock": {
      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",
      "number": "18000003",
      "timestamp": "2023-08-26T16:22:11Z"
    },
    "events": [
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000180cd4303582d348a54fcb0436d5665f082a23d1",
            "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43"
          ],
          "data": "0x00000000000000000000000000000000000000000000000000000000aad0a340",
          "blockIndex": 222,
          "ordinal": "4219"
        },
        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"
      },
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x0000000000000000000000001c727a55ea3c11b0ab7d3a361fe0f3c47ce6de5d",
            "0x000000000000000000000000c3dcc088aef4373e665b00fc8be45a4b6a4b24a6"
          ],
          "data": "0x000000000000000000000000000000000000000000000000000000000686bcda",
          "blockIndex": 230,
          "ordinal": "4543"
        },
        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"
      }
    ]
  }
}
{
  "@module": "filtered_events",
  "@block": 18000004,
  "@type": "sf.substreams.ethereum.v1.Events",
  "@data": {
    "clock": {
      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",
      "number": "18000004",
      "timestamp": "2023-08-26T16:22:23Z"
    },
    "events": [
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x0000000000000000000000000000000000000000000000000000000000000000",
            "0x00000000000000000000000043954785980c50768373dc1dafd7ba3a2052036f"
          ],
          "data": "0x000000000000000000000000000000000000000000000000000000e8d433aa6e",
          "index": 1,
          "blockIndex": 191,
          "ordinal": "3060"
        },
        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"
      }
    ]
  }
}
{
  "@module": "filtered_events",
  "@block": 18000005,
  "@type": "sf.substreams.ethereum.v1.Events",
  "@data": {
    "clock": {
      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",
      "number": "18000005",
      "timestamp": "2023-08-26T16:22:35Z"
    },
    "events": [
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000c880d8be4e94f644f1daa7d3558b50ff5a1c2db7",
            "0x000000000000000000000000ae2d4617c862309a3d75a0ffb358c7a5009c673f"
          ],
          "data": "0x0000000000000000000000000000000000000000000000000000002e90edd000",
          "blockIndex": 64,
          "ordinal": "1110"
        },
        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"
      },
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000b86fc5e985facdcae6034beefc6cbdcd8bbd8cb4",
            "0x00000000000000000000000027f713adefc708469d826ab8f5b1e4672f1a070e"
          ],
          "data": "0x0000000000000000000000000000000000000000000000000000000059682f00",
          "blockIndex": 203,
          "ordinal": "3669"
        },
        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"
      }
    ]
  }
}
{
  "@module": "filtered_events",
  "@block": 18000006,
  "@type": "sf.substreams.ethereum.v1.Events",
  "@data": {
    "clock": {
      "id": "ed05b59286d4f2db801e78a23ce4584d869d857b70b2758822844427caa55a85",
      "number": "18000006",
      "timestamp": "2023-08-26T16:22:47Z"
    }
  }
}
{
  "@module": "filtered_events",
  "@block": 18000007,
  "@type": "sf.substreams.ethereum.v1.Events",
  "@data": {
    "clock": {
      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",
      "number": "18000007",
      "timestamp": "2023-08-26T16:22:59Z"
    },
    "events": [
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000b4eae88d1643e172518dde0ef213c20628798c65",
            "0x00000000000000000000000038c5a4f22e2e290ba9bbb9e3d9aaf8582f79f4d1"
          ],
          "data": "0x0000000000000000000000000000000000000000000000000000000006b78840",
          "blockIndex": 94,
          "ordinal": "1448"
        },
        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"
      },
      {
        "log": {
          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000579752cff8fee7af09446b2133ee2f9ff10c4fbf",
            "0x0000000000000000000000001715a3e4a142d8b698131108995174f37aeba10d"
          ],
          "data": "0x000000000000000000000000000000000000000000000000000000001d0221cd",
          "blockIndex": 179,
          "ordinal": "3398"
        },
        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"
      }
    ]
  }
}
```

Historical block: 18,000,003

Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON

ERC-20 Transfer signature

Sender address, left-padded to 32 bytes

Recipient address, left-padded to 32 bytes

Transfer amount: 2,865,800,000 base units, 2,865.8 USDC

Transaction containing this log

TypeScript · @substreams/core

Prerequisites

1. Install Node.js.

2. Install @connectrpc/connect, @connectrpc/connect-node, @bufbuild/protobuf, @substreams/core, and tsx. `npm install @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf @substreams/core tsx`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Node.js 24.22.1, @connectrpc/connect 1.7.0, @connectrpc/connect-node 1.7.0, ethereum-common v0.4.0, @bufbuild/protobuf 1.10.1, @substreams/core 0.17.0, tsx 4.23.15.

[Example project](/examples/substreams.api.ts-5.tar.gz)

```typescript
import { createHash } from "node:crypto";
import { createGrpcTransport, Http2SessionManager } from "@connectrpc/connect-node";
import {
  applyParams,
  createRegistry,
  createRequest,
  createSubstream,
  streamBlocks,
  unpackMapOutput,
} from "@substreams/core";

const endpoint = "https://1.evm.substreams.graphops.xyz";
const apiKey = process.env.GRAPHOPS_API_KEY;
if (!apiKey) throw new Error("Set GRAPHOPS_API_KEY");

// Check the package's SHA-256, so the program only runs the exact module code you reviewed.
const download = await fetch("https://spkg.io/v1/packages/ethereum-common/v0.4.0");
if (!download.ok) throw new Error(`Package download failed: ${download.status}`);
const bytes = new Uint8Array(await download.arrayBuffer());
const checksum = createHash("sha256").update(bytes).digest("hex");
if (checksum !== "beda75cef8a655941816a7c2be222ed590f618fe389e7c08c3ac5f0826161345") {
  throw new Error("Package checksum mismatch");
}

const pkg = createSubstream(bytes);
if (!pkg.modules) throw new Error("Package has no modules");
// Keep Transfer events (evt_sig) emitted by the USDC contract (evt_addr).
applyParams(
  [
    "filtered_events=evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
  ],
  pkg.modules.modules,
);
// The package embeds its protobuf schemas, so the output decodes without generated code.
const registry = createRegistry(pkg);

const request = createRequest({
  substreamPackage: pkg,
  outputModule: "filtered_events",
  startBlockNum: 18000002,
  stopBlockNum: 18000008, // exclusive
  // Unset starts at startBlockNum; a committed cursor resumes right after its block.
  startCursor: process.env.GRAPHOPS_CURSOR || undefined,
  // Production mode processes the range in parallel and sends only the output module.
  productionMode: true,
  // Only irreversible blocks, so the server never sends a blockUndoSignal (reorg rewind).
  finalBlocksOnly: true,
});

const sessionManager = new Http2SessionManager(endpoint);
const transport = createGrpcTransport({ baseUrl: endpoint, httpVersion: "2", sessionManager });
// GraphOps authenticates every gRPC call with the x-api-key metadata header.
const headers = { "x-api-key": apiKey };
try {
  for await (const response of streamBlocks(transport, request, { headers })) {
    // Skip session, progress and other non-data messages.
    if (response.message.case !== "blockScopedData") continue;
    const { clock, cursor } = response.message.value;
    const output = unpackMapOutput(response, registry);
    const data = output?.toJson() as { events?: unknown[] } | undefined;
    if (!data?.events) continue; // no matching transfers in this block

    // Store the events and this cursor together; after a restart, pass the last stored
    // cursor as startCursor to continue from the next block without gaps or duplicates.
    console.log(JSON.stringify({ block: String(clock?.number), cursor, data }));
  }
} finally {
  sessionManager.abort(); // Close the HTTP/2 connection so the process can exit.
}

```

Output

```json
{
  "block": "18000002",
  "cursor": "89c_0WDIRTi08Zu_7v-ibqWwLpc_DFtnVQvlKhFDj46k-XaW3pT3VGYgPU6Cxqn03kPtSQuoj9jLR3sqoJUFtNbryr1m7SZsEHIkwtzv_eG9e6XwMQNJc7lkWLyPZtnRWjTXYgr7erMK5YfuPqDabhNmZZJzfzTljztT9INWIadAvnVmxm2vJpra1_qf9IBB-LZzTezwwCqlUmd-ek1SNJ6AZfSY6T0kZA==",
  "data": {
    "clock": {
      "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141",
      "number": "18000002",
      "timestamp": "2023-08-26T16:21:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAA5nO9oZpSnZ/xOu3a4OCMD9qLVIg=",
            "AAAAAAAAAAAAAAAAiOagwt3Sb+62TwOaLEEpb8s/VkA="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEATMsA=",
          "index": 1,
          "blockIndex": 172,
          "ordinal": "2582"
        },
        "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAApAuxxH9t0nFCor18k7+pjbnR9cU=",
            "AAAAAAAAAAAAAAAAtOFtAWjlLTXKzSxhhbRCgewoydw="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABE0jl0=",
          "index": 2,
          "blockIndex": 315,
          "ordinal": "5139"
        },
        "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db"
      }
    ]
  }
}
{
  "block": "18000003",
  "cursor": "n-jOoTkc7lESaOaRPLPCC6WwLpc_DFtnVQvlKhFCj4L2o3LDjJTwUTVzbkmFkqj3jkDuQ1yuj9bPRXwv9sUBtIfuwe5tvHNuTi5_ko66_ua5Kvv3b1xPIrxqXOzdM9zRWjTXYgr7erIK6dW0OvWIbhRjNsEgeDOxjjgD94BcdqFAsHFkwWj5dp7ahv-Up4sQrbQtEbegkn-mVWMvJEoMa5jRYPqcuW9xYQ==",
  "data": {
    "clock": {
      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",
      "number": "18000003",
      "timestamp": "2023-08-26T16:22:11Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAGAzUMDWC00ilT8sENtVmXwgqI9E=",
            "AAAAAAAAAAAAAAAAqdHgjHeTr2fp2S/jCNVpf7gdPkM="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrQo0A=",
          "blockIndex": 222,
          "ordinal": "4219"
        },
        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAHHJ6Veo8EbCrfTo2H+DzxHzm3l0=",
            "AAAAAAAAAAAAAAAAw9zAiK70Nz5mWwD8i+RaS2pLJKY="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGvNo=",
          "blockIndex": 230,
          "ordinal": "4543"
        },
        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"
      }
    ]
  }
}
{
  "block": "18000004",
  "cursor": "egpIfwVCY1AAna0iQWwoTaWwLpc_DFtnVQvlKhFFj4yg93rN1M6lBmlzPBqGla_wiUHtQlr9i43PRX97oMRZ6dK7xL5s6SU9Qnh_xY7mqOfvLKWnPQwTdu5gVuKLNtrRWjTXYgr7erUK54PgMvvQNEE0asFyKzC2iT8E9oNdcPJE63Fkwjyvd8aH06qR94pF--chR7f3kiPwVDUpehpeO8SFMvCWtzl0Zw==",
  "data": {
    "clock": {
      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",
      "number": "18000004",
      "timestamp": "2023-08-26T16:22:23Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
            "AAAAAAAAAAAAAAAAQ5VHhZgMUHaDc9wdr9e6OiBSA28="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6NQzqm4=",
          "index": 1,
          "blockIndex": 191,
          "ordinal": "3060"
        },
        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"
      }
    ]
  }
}
{
  "block": "18000005",
  "cursor": "ZLBOgvAzsxIgZXZxiKsc4aWwLpc_DFtnVQvlKhFEj4r09yHDj5ShCWV9bxmEl6nx3BLsSwivj47PRX4q9MRQ6dC-kbpk7HI6En0rlN3u-ubtKvCgOANOduo2W-iJMd_RWjTXYgr7erQK4dfgafWLbkU7Zs8hKDK0jz5RpYJUIqBA6HFkw237d8-H0a_E84JArOBxQuOmwSuiVTcvLx1bNJmFNqabvTtzYg==",
  "data": {
    "clock": {
      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",
      "number": "18000005",
      "timestamp": "2023-08-26T16:22:35Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAyIDYvk6U9kTx2qfTVYtQ/1ocLbc=",
            "AAAAAAAAAAAAAAAAri1GF8hiMJo9daD/s1jHpQCcZz8="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALpDt0AA=",
          "blockIndex": 64,
          "ordinal": "1110"
        },
        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAuG/F6YX6zcrmA0vu/Gy9zYu9jLQ=",
            "AAAAAAAAAAAAAAAAJ/cTre/HCEadgmq49bHkZy8aBw4="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFloLwA=",
          "blockIndex": 203,
          "ordinal": "3669"
        },
        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"
      }
    ]
  }
}
{
  "block": "18000007",
  "cursor": "JtpB3QUXuAcCSUDww-4QDqWwLpc_DFtnVQvlKhFGj9-k9nGQ35v0BmRwOUnTkf323UfpSl2vjd_PFXx888hQ6YXuxepm5SQ4QC0rm4y_-bLneqbyPAJIIL1jDrzaMdnRWjTXYgr7erYKtIfhOabbYRA0Z8J3eGWy2zlQ8IdVd6BCuXE0wTv8e8-HhP-Qo4BJ-uIjEuOpkHqhAT1_eU9fNZ_TYfPO6WhzZA==",
  "data": {
    "clock": {
      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",
      "number": "18000007",
      "timestamp": "2023-08-26T16:22:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAtOrojRZD4XJRjd4O8hPCBih5jGU=",
            "AAAAAAAAAAAAAAAAOMWk8i4uKQupu7nj2ar4WC959NE="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa3iEA=",
          "blockIndex": 94,
          "ordinal": "1448"
        },
        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAV5dSz/j+568JRGshM+4vn/EMT78=",
            "AAAAAAAAAAAAAAAAFxWj5KFC2LaYExEImVF083rroQ0="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0CIc0=",
          "blockIndex": 179,
          "ordinal": "3398"
        },
        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"
      }
    ]
  }
}
```

Historical block: 18,000,002

Commit this cursor with the block’s events; resume from it after a disconnect

Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON

ERC-20 Transfer signature

Sender address, left-padded to 32 bytes

Recipient address, left-padded to 32 bytes

Transfer amount: 1,075,000,000 base units, 1,075 USDC

Transaction containing this log

Go · substreams

Prerequisites

1. Install Go.

2. Install github.com/streamingfast/substreams. `go get github.com/streamingfast/substreams`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Go 1.26.0, ethereum-common v0.4.0, github.com/streamingfast/substreams v1.25.0.

[Example project](/examples/substreams.api.go-3.tar.gz)

```go
package main

import (
	"context"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	rpc "github.com/streamingfast/substreams/pb/sf/substreams/rpc/v2"
	pb "github.com/streamingfast/substreams/pb/sf/substreams/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/descriptorpb"
	"google.golang.org/protobuf/types/dynamicpb"
)

const (
	endpoint      = "https://1.evm.substreams.graphops.xyz"
	packageURL    = "https://spkg.io/v1/packages/ethereum-common/v0.4.0"
	packageSHA256 = "beda75cef8a655941816a7c2be222ed590f618fe389e7c08c3ac5f0826161345"
	module        = "filtered_events"
	outputType    = "sf.substreams.ethereum.v1.Events"
	// Keep Transfer events (evt_sig) emitted by the USDC contract (evt_addr).
	params = "evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	apiKey := os.Getenv("GRAPHOPS_API_KEY")
	if apiKey == "" {
		return errors.New("set GRAPHOPS_API_KEY")
	}

	pkg, err := downloadPackage()
	if err != nil {
		return err
	}
	// Set the module's params input, like `substreams run --params`.
	for _, m := range pkg.Modules.Modules {
		if m.Name != module {
			continue
		}
		for _, input := range m.Inputs {
			if p := input.GetParams(); p != nil {
				p.Value = params
			}
		}
	}

	// The package embeds its protobuf schemas, so the output decodes without generated code.
	files, err := protodesc.NewFiles(&descriptorpb.FileDescriptorSet{File: pkg.ProtoFiles})
	if err != nil {
		return err
	}
	descriptor, err := files.FindDescriptorByName(outputType)
	if err != nil {
		return err
	}
	eventsType := descriptor.(protoreflect.MessageDescriptor)

	// Trust the system roots, or the CA bundle in SSL_CERT_FILE when set (Go ignores it on macOS).
	creds := credentials.NewTLS(&tls.Config{})
	if caFile := os.Getenv("SSL_CERT_FILE"); caFile != "" {
		if creds, err = credentials.NewClientTLSFromFile(caFile, ""); err != nil {
			return err
		}
	}
	conn, err := grpc.NewClient(strings.TrimPrefix(endpoint, "https://"), grpc.WithTransportCredentials(creds))
	if err != nil {
		return err
	}
	defer conn.Close()

	// GraphOps authenticates every gRPC call with the x-api-key metadata header.
	ctx := metadata.AppendToOutgoingContext(context.Background(), "x-api-key", apiKey)
	stream, err := rpc.NewStreamClient(conn).Blocks(ctx, &rpc.Request{
		Modules:       pkg.Modules,
		OutputModule:  module,
		StartBlockNum: 18000002,
		StopBlockNum:  18000008, // exclusive
		// Empty starts at StartBlockNum; a committed cursor resumes right after its block.
		StartCursor: os.Getenv("GRAPHOPS_CURSOR"),
		// Production mode processes the range in parallel and sends only the output module.
		ProductionMode: true,
		// Only irreversible blocks, so the server never sends a BlockUndoSignal (reorg rewind).
		FinalBlocksOnly: true,
	})
	if err != nil {
		return err
	}

	for {
		response, err := stream.Recv()
		if errors.Is(err, io.EOF) {
			return nil // reached StopBlockNum
		}
		if err != nil {
			return err
		}
		data := response.GetBlockScopedData()
		if data == nil {
			continue // session, progress and other non-data messages
		}

		output := dynamicpb.NewMessage(eventsType)
		if err := proto.Unmarshal(data.GetOutput().GetMapOutput().GetValue(), output); err != nil {
			return err
		}
		outputJSON, err := protojson.Marshal(output)
		if err != nil {
			return err
		}
		var payload map[string]any
		if err := json.Unmarshal(outputJSON, &payload); err != nil {
			return err
		}
		if payload["events"] == nil {
			continue // no matching transfers in this block
		}

		// Store the events and this cursor together; after a restart, pass the last stored
		// cursor as StartCursor to continue from the next block without gaps or duplicates.
		record := map[string]any{"block": fmt.Sprint(data.Clock.Number), "cursor": data.Cursor, "data": payload}
		if err := json.NewEncoder(os.Stdout).Encode(record); err != nil {
			return err
		}
	}
}

// downloadPackage fetches the Substreams package and checks its SHA-256, so the program only
// ever runs the exact module code that was reviewed.
func downloadPackage() (*pb.Package, error) {
	response, err := http.Get(packageURL)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("package download: %s", response.Status)
	}
	content, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	if sum := sha256.Sum256(content); hex.EncodeToString(sum[:]) != packageSHA256 {
		return nil, errors.New("package checksum mismatch")
	}
	pkg := &pb.Package{}
	return pkg, proto.Unmarshal(content, pkg)
}

```

Output

```json
{
  "block": "18000002",
  "cursor": "89c_0WDIRTi08Zu_7v-ibqWwLpc_DFtnVQvlKhFDj46k-XaW3pT3VGYgPU6Cxqn03kPtSQuoj9jLR3sqoJUFtNbryr1m7SZsEHIkwtzv_eG9e6XwMQNJc7lkWLyPZtnRWjTXYgr7erMK5YfuPqDabhNmZZJzfzTljztT9INWIadAvnVmxm2vJpra1_qf9IBB-LZzTezwwCqlUmd-ek1SNJ6AZfSY6T0kZA==",
  "data": {
    "clock": {
      "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141",
      "number": "18000002",
      "timestamp": "2023-08-26T16:21:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 172,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEATMsA=",
          "index": 1,
          "ordinal": "2582",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAA5nO9oZpSnZ/xOu3a4OCMD9qLVIg=",
            "AAAAAAAAAAAAAAAAiOagwt3Sb+62TwOaLEEpb8s/VkA="
          ]
        },
        "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 315,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABE0jl0=",
          "index": 2,
          "ordinal": "5139",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAApAuxxH9t0nFCor18k7+pjbnR9cU=",
            "AAAAAAAAAAAAAAAAtOFtAWjlLTXKzSxhhbRCgewoydw="
          ]
        },
        "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db"
      }
    ]
  }
}
{
  "block": "18000003",
  "cursor": "n-jOoTkc7lESaOaRPLPCC6WwLpc_DFtnVQvlKhFCj4L2o3LDjJTwUTVzbkmFkqj3jkDuQ1yuj9bPRXwv9sUBtIfuwe5tvHNuTi5_ko66_ua5Kvv3b1xPIrxqXOzdM9zRWjTXYgr7erIK6dW0OvWIbhRjNsEgeDOxjjgD94BcdqFAsHFkwWj5dp7ahv-Up4sQrbQtEbegkn-mVWMvJEoMa5jRYPqcuW9xYQ==",
  "data": {
    "clock": {
      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",
      "number": "18000003",
      "timestamp": "2023-08-26T16:22:11Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 222,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrQo0A=",
          "ordinal": "4219",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAGAzUMDWC00ilT8sENtVmXwgqI9E=",
            "AAAAAAAAAAAAAAAAqdHgjHeTr2fp2S/jCNVpf7gdPkM="
          ]
        },
        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 230,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGvNo=",
          "ordinal": "4543",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAHHJ6Veo8EbCrfTo2H+DzxHzm3l0=",
            "AAAAAAAAAAAAAAAAw9zAiK70Nz5mWwD8i+RaS2pLJKY="
          ]
        },
        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"
      }
    ]
  }
}
{
  "block": "18000004",
  "cursor": "egpIfwVCY1AAna0iQWwoTaWwLpc_DFtnVQvlKhFFj4yg93rN1M6lBmlzPBqGla_wiUHtQlr9i43PRX97oMRZ6dK7xL5s6SU9Qnh_xY7mqOfvLKWnPQwTdu5gVuKLNtrRWjTXYgr7erUK54PgMvvQNEE0asFyKzC2iT8E9oNdcPJE63Fkwjyvd8aH06qR94pF--chR7f3kiPwVDUpehpeO8SFMvCWtzl0Zw==",
  "data": {
    "clock": {
      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",
      "number": "18000004",
      "timestamp": "2023-08-26T16:22:23Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 191,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6NQzqm4=",
          "index": 1,
          "ordinal": "3060",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
            "AAAAAAAAAAAAAAAAQ5VHhZgMUHaDc9wdr9e6OiBSA28="
          ]
        },
        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"
      }
    ]
  }
}
{
  "block": "18000005",
  "cursor": "ZLBOgvAzsxIgZXZxiKsc4aWwLpc_DFtnVQvlKhFEj4r09yHDj5ShCWV9bxmEl6nx3BLsSwivj47PRX4q9MRQ6dC-kbpk7HI6En0rlN3u-ubtKvCgOANOduo2W-iJMd_RWjTXYgr7erQK4dfgafWLbkU7Zs8hKDK0jz5RpYJUIqBA6HFkw237d8-H0a_E84JArOBxQuOmwSuiVTcvLx1bNJmFNqabvTtzYg==",
  "data": {
    "clock": {
      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",
      "number": "18000005",
      "timestamp": "2023-08-26T16:22:35Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 64,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALpDt0AA=",
          "ordinal": "1110",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAyIDYvk6U9kTx2qfTVYtQ/1ocLbc=",
            "AAAAAAAAAAAAAAAAri1GF8hiMJo9daD/s1jHpQCcZz8="
          ]
        },
        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 203,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFloLwA=",
          "ordinal": "3669",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAuG/F6YX6zcrmA0vu/Gy9zYu9jLQ=",
            "AAAAAAAAAAAAAAAAJ/cTre/HCEadgmq49bHkZy8aBw4="
          ]
        },
        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"
      }
    ]
  }
}
{
  "block": "18000007",
  "cursor": "JtpB3QUXuAcCSUDww-4QDqWwLpc_DFtnVQvlKhFGj9-k9nGQ35v0BmRwOUnTkf323UfpSl2vjd_PFXx888hQ6YXuxepm5SQ4QC0rm4y_-bLneqbyPAJIIL1jDrzaMdnRWjTXYgr7erYKtIfhOabbYRA0Z8J3eGWy2zlQ8IdVd6BCuXE0wTv8e8-HhP-Qo4BJ-uIjEuOpkHqhAT1_eU9fNZ_TYfPO6WhzZA==",
  "data": {
    "clock": {
      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",
      "number": "18000007",
      "timestamp": "2023-08-26T16:22:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 94,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa3iEA=",
          "ordinal": "1448",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAtOrojRZD4XJRjd4O8hPCBih5jGU=",
            "AAAAAAAAAAAAAAAAOMWk8i4uKQupu7nj2ar4WC959NE="
          ]
        },
        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 179,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0CIc0=",
          "ordinal": "3398",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAV5dSz/j+568JRGshM+4vn/EMT78=",
            "AAAAAAAAAAAAAAAAFxWj5KFC2LaYExEImVF083rroQ0="
          ]
        },
        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"
      }
    ]
  }
}
```

Historical block: 18,000,002

Commit this cursor with the block’s events; resume from it after a disconnect

Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON

Transfer amount: 1,075,000,000 base units, 1,075 USDC

ERC-20 Transfer signature

Sender address, left-padded to 32 bytes

Recipient address, left-padded to 32 bytes

Transaction containing this log

Rust · Buffa

Prerequisites

1. Install Rust.

2. Install buffa, buffa-descriptor, buffa-types, connectrpc, reqwest, ring, rustls-native-certs, serde, serde\_json, and tokio. `cargo add buffa buffa-descriptor buffa-types connectrpc reqwest ring rustls-native-certs serde serde_json tokio -F buffa/json -F buffa-descriptor/json -F buffa-descriptor/reflect -F buffa-descriptor/views -F buffa-types/json -F connectrpc/client-tls -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread`

3. Install connectrpc-build. `cargo add --build connectrpc-build`

4. Use the Substreams protocol bindings included in the example project, or generate them from https\://github.com/streamingfast/substreams/tree/v1.25.0/proto.

5. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Rust 1.98.1, buffa 0.9.2, buffa-descriptor 0.9.2, buffa-types 0.9.2, connectrpc 0.9.1, connectrpc-build 0.9.0, ethereum-common v0.4.0, reqwest 0.13.5, ring 0.17.14, rustls-native-certs 0.8.5, serde 1.0.228, serde\_json 1.0.145, Substreams protocol bindings v1.25.0, tokio 1.48.0.

[Example project](/examples/substreams.api.rust-4.tar.gz)

```rust
// build.rs generates the Substreams protocol types and client with Buffa and connectrpc.
mod pb {
    connectrpc::include_generated!();
}

use buffa::Message;
use buffa_descriptor::{generated::descriptor::FileDescriptorSet, DescriptorPool, DynamicMessage};
use connectrpc::client::{ClientConfig, HttpClient};
use connectrpc::{rustls, Protocol};
use pb::sf::substreams::rpc::v2::{response, Request, StreamClient};
use pb::sf::substreams::v1::{module::input::Input, Package};
use ring::digest::{digest, SHA256};
use serde_json::json;
use std::{env, error::Error, sync::Arc};

const ENDPOINT: &str = "https://1.evm.substreams.graphops.xyz";
const PACKAGE_URL: &str = "https://spkg.io/v1/packages/ethereum-common/v0.4.0";
const PACKAGE_SHA256: &str = "beda75cef8a655941816a7c2be222ed590f618fe389e7c08c3ac5f0826161345";
const MODULE: &str = "filtered_events";
const OUTPUT_TYPE: &str = "sf.substreams.ethereum.v1.Events";
// Keep Transfer events (evt_sig) emitted by the USDC contract (evt_addr).
const PARAMS: &str = "evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let api_key = env::var("GRAPHOPS_API_KEY")?;

    // Check the package's SHA-256, so the program only runs the exact module code you reviewed.
    let bytes = reqwest::get(PACKAGE_URL)
        .await?
        .error_for_status()?
        .bytes()
        .await?;
    let checksum: String = digest(&SHA256, &bytes)
        .as_ref()
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect();
    if checksum != PACKAGE_SHA256 {
        return Err("package checksum mismatch".into());
    }
    let mut package = Package::decode_from_slice(&bytes)?;

    // Set the module's params input, like `substreams run --params`.
    let module = package
        .modules
        .as_option_mut()
        .ok_or("package has no modules")?
        .modules
        .iter_mut()
        .find(|module| module.name == MODULE)
        .ok_or("module not found")?;
    for input in &mut module.inputs {
        if let Some(Input::Params(params)) = &mut input.input {
            params.value = PARAMS.into();
        }
    }

    // The package embeds its protobuf schemas, so the output decodes without generated code.
    let pool = Arc::new(DescriptorPool::new(FileDescriptorSet {
        file: std::mem::take(&mut package.proto_files),
        ..Default::default()
    })?);
    let events_type = pool
        .message_index(OUTPUT_TYPE)
        .ok_or("output type not found")?;

    // Trust the system roots, or the CA bundle in SSL_CERT_FILE when set.
    let mut roots = rustls::RootCertStore::empty();
    roots.add_parsable_certificates(rustls_native_certs::load_native_certs().certs);
    let tls = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();
    // GraphOps authenticates every gRPC call with the x-api-key metadata header.
    let config = ClientConfig::new(ENDPOINT.parse()?)
        .with_protocol(Protocol::Grpc)
        .with_default_header("x-api-key", api_key);
    let client = StreamClient::new(HttpClient::with_tls(Arc::new(tls)), config);

    let mut stream = client
        .blocks(Request {
            modules: package.modules,
            output_module: MODULE.into(),
            start_block_num: 18000002,
            stop_block_num: 18000008, // exclusive
            // Empty starts at start_block_num; a committed cursor resumes right after its block.
            start_cursor: env::var("GRAPHOPS_CURSOR").unwrap_or_default(),
            // Production mode processes the range in parallel and sends only the output module.
            production_mode: true,
            // Only irreversible blocks, so the server never sends a BlockUndoSignal (reorg rewind).
            final_blocks_only: true,
            ..Default::default()
        })
        .await?;
    while let Some(message) = stream.message().await? {
        // Skip session, progress and other non-data messages.
        let Some(response::Message::BlockScopedData(data)) = message.to_owned_message().message
        else {
            continue;
        };
        let output = &data.output.map_output;
        let events = DynamicMessage::decode(pool.clone(), events_type, &output.value)?;
        let events = serde_json::to_value(&events)?;
        if events.get("events").is_none() {
            continue; // no matching transfers in this block
        }

        // Store the events and this cursor together; after a restart, pass the last stored
        // cursor as start_cursor to continue from the next block without gaps or duplicates.
        let record = json!({
            "block": data.clock.number.to_string(),
            "cursor": data.cursor,
            "data": events,
        });
        println!("{record}");
    }
    Ok(())
}

```

Output

```json
{
  "block": "18000002",
  "cursor": "89c_0WDIRTi08Zu_7v-ibqWwLpc_DFtnVQvlKhFDj46k-XaW3pT3VGYgPU6Cxqn03kPtSQuoj9jLR3sqoJUFtNbryr1m7SZsEHIkwtzv_eG9e6XwMQNJc7lkWLyPZtnRWjTXYgr7erMK5YfuPqDabhNmZZJzfzTljztT9INWIadAvnVmxm2vJpra1_qf9IBB-LZzTezwwCqlUmd-ek1SNJ6AZfSY6T0kZA==",
  "data": {
    "clock": {
      "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141",
      "number": "18000002",
      "timestamp": "2023-08-26T16:21:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 172,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEATMsA=",
          "index": 1,
          "ordinal": "2582",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAA5nO9oZpSnZ/xOu3a4OCMD9qLVIg=",
            "AAAAAAAAAAAAAAAAiOagwt3Sb+62TwOaLEEpb8s/VkA="
          ]
        },
        "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 315,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABE0jl0=",
          "index": 2,
          "ordinal": "5139",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAApAuxxH9t0nFCor18k7+pjbnR9cU=",
            "AAAAAAAAAAAAAAAAtOFtAWjlLTXKzSxhhbRCgewoydw="
          ]
        },
        "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db"
      }
    ]
  }
}
{
  "block": "18000003",
  "cursor": "n-jOoTkc7lESaOaRPLPCC6WwLpc_DFtnVQvlKhFCj4L2o3LDjJTwUTVzbkmFkqj3jkDuQ1yuj9bPRXwv9sUBtIfuwe5tvHNuTi5_ko66_ua5Kvv3b1xPIrxqXOzdM9zRWjTXYgr7erIK6dW0OvWIbhRjNsEgeDOxjjgD94BcdqFAsHFkwWj5dp7ahv-Up4sQrbQtEbegkn-mVWMvJEoMa5jRYPqcuW9xYQ==",
  "data": {
    "clock": {
      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",
      "number": "18000003",
      "timestamp": "2023-08-26T16:22:11Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 222,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrQo0A=",
          "ordinal": "4219",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAGAzUMDWC00ilT8sENtVmXwgqI9E=",
            "AAAAAAAAAAAAAAAAqdHgjHeTr2fp2S/jCNVpf7gdPkM="
          ]
        },
        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 230,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGvNo=",
          "ordinal": "4543",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAHHJ6Veo8EbCrfTo2H+DzxHzm3l0=",
            "AAAAAAAAAAAAAAAAw9zAiK70Nz5mWwD8i+RaS2pLJKY="
          ]
        },
        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"
      }
    ]
  }
}
{
  "block": "18000004",
  "cursor": "egpIfwVCY1AAna0iQWwoTaWwLpc_DFtnVQvlKhFFj4yg93rN1M6lBmlzPBqGla_wiUHtQlr9i43PRX97oMRZ6dK7xL5s6SU9Qnh_xY7mqOfvLKWnPQwTdu5gVuKLNtrRWjTXYgr7erUK54PgMvvQNEE0asFyKzC2iT8E9oNdcPJE63Fkwjyvd8aH06qR94pF--chR7f3kiPwVDUpehpeO8SFMvCWtzl0Zw==",
  "data": {
    "clock": {
      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",
      "number": "18000004",
      "timestamp": "2023-08-26T16:22:23Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 191,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6NQzqm4=",
          "index": 1,
          "ordinal": "3060",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
            "AAAAAAAAAAAAAAAAQ5VHhZgMUHaDc9wdr9e6OiBSA28="
          ]
        },
        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"
      }
    ]
  }
}
{
  "block": "18000005",
  "cursor": "ZLBOgvAzsxIgZXZxiKsc4aWwLpc_DFtnVQvlKhFEj4r09yHDj5ShCWV9bxmEl6nx3BLsSwivj47PRX4q9MRQ6dC-kbpk7HI6En0rlN3u-ubtKvCgOANOduo2W-iJMd_RWjTXYgr7erQK4dfgafWLbkU7Zs8hKDK0jz5RpYJUIqBA6HFkw237d8-H0a_E84JArOBxQuOmwSuiVTcvLx1bNJmFNqabvTtzYg==",
  "data": {
    "clock": {
      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",
      "number": "18000005",
      "timestamp": "2023-08-26T16:22:35Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 64,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALpDt0AA=",
          "ordinal": "1110",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAyIDYvk6U9kTx2qfTVYtQ/1ocLbc=",
            "AAAAAAAAAAAAAAAAri1GF8hiMJo9daD/s1jHpQCcZz8="
          ]
        },
        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 203,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFloLwA=",
          "ordinal": "3669",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAuG/F6YX6zcrmA0vu/Gy9zYu9jLQ=",
            "AAAAAAAAAAAAAAAAJ/cTre/HCEadgmq49bHkZy8aBw4="
          ]
        },
        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"
      }
    ]
  }
}
{
  "block": "18000007",
  "cursor": "JtpB3QUXuAcCSUDww-4QDqWwLpc_DFtnVQvlKhFGj9-k9nGQ35v0BmRwOUnTkf323UfpSl2vjd_PFXx888hQ6YXuxepm5SQ4QC0rm4y_-bLneqbyPAJIIL1jDrzaMdnRWjTXYgr7erYKtIfhOabbYRA0Z8J3eGWy2zlQ8IdVd6BCuXE0wTv8e8-HhP-Qo4BJ-uIjEuOpkHqhAT1_eU9fNZ_TYfPO6WhzZA==",
  "data": {
    "clock": {
      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",
      "number": "18000007",
      "timestamp": "2023-08-26T16:22:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 94,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa3iEA=",
          "ordinal": "1448",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAtOrojRZD4XJRjd4O8hPCBih5jGU=",
            "AAAAAAAAAAAAAAAAOMWk8i4uKQupu7nj2ar4WC959NE="
          ]
        },
        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "blockIndex": 179,
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0CIc0=",
          "ordinal": "3398",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAV5dSz/j+568JRGshM+4vn/EMT78=",
            "AAAAAAAAAAAAAAAAFxWj5KFC2LaYExEImVF083rroQ0="
          ]
        },
        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"
      }
    ]
  }
}
```

Historical block: 18,000,002

Commit this cursor with the block’s events; resume from it after a disconnect

Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON

Transfer amount: 1,075,000,000 base units, 1,075 USDC

ERC-20 Transfer signature

Sender address, left-padded to 32 bytes

Recipient address, left-padded to 32 bytes

Transaction containing this log

Python · grpcio

Prerequisites

1. Install Python.

2. Install grpcio, protobuf, and requests. `python -m pip install grpcio protobuf requests`

3. Use the Substreams protocol bindings included in the example project, or generate them from https\://github.com/streamingfast/substreams/tree/v1.25.0/proto.

4. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Python 3.12.12, ethereum-common v0.4.0, grpcio 1.84.0, protobuf 7.36.2, requests 2.34.2, Substreams protocol bindings v1.25.0.

[Example project](/examples/substreams.api.python-6.tar.gz)

```python
import hashlib
import json
import os

import grpc
import requests
from google.protobuf import descriptor_pool, json_format, message_factory

# Generated Substreams protocol bindings, included in the example project.
from sf.substreams.rpc.v2 import service_pb2, service_pb2_grpc
from sf.substreams.v1 import package_pb2

ENDPOINT = "https://1.evm.substreams.graphops.xyz"
PACKAGE_URL = "https://spkg.io/v1/packages/ethereum-common/v0.4.0"
PACKAGE_SHA256 = "beda75cef8a655941816a7c2be222ed590f618fe389e7c08c3ac5f0826161345"
MODULE = "filtered_events"
OUTPUT_TYPE = "sf.substreams.ethereum.v1.Events"
# Keep Transfer events (evt_sig) emitted by the USDC contract (evt_addr).
PARAMS = "evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

api_key = os.environ["GRAPHOPS_API_KEY"]

# Check the package's SHA-256, so the program only runs the exact module code you reviewed.
download = requests.get(PACKAGE_URL, timeout=30)
download.raise_for_status()
if hashlib.sha256(download.content).hexdigest() != PACKAGE_SHA256:
    raise RuntimeError("Package checksum mismatch")
package = package_pb2.Package.FromString(download.content)

# Set the module's params input, like `substreams run --params`.
for module in package.modules.modules:
    if module.name == MODULE:
        for module_input in module.inputs:
            if module_input.HasField("params"):
                module_input.params.value = PARAMS

# The package embeds its protobuf schemas, so the output decodes without generated code.
pool = descriptor_pool.DescriptorPool()
for proto_file in package.proto_files:
    pool.Add(proto_file)
Events = message_factory.GetMessageClass(pool.FindMessageTypeByName(OUTPUT_TYPE))

# Trust gRPC's default roots, or the CA bundle in SSL_CERT_FILE when set.
roots = None
if os.environ.get("SSL_CERT_FILE"):
    with open(os.environ["SSL_CERT_FILE"], "rb") as bundle:
        roots = bundle.read()
credentials = grpc.ssl_channel_credentials(root_certificates=roots)

with grpc.secure_channel(ENDPOINT.removeprefix("https://"), credentials) as channel:
    request = service_pb2.Request(
        modules=package.modules,
        output_module=MODULE,
        start_block_num=18000002,
        stop_block_num=18000008,  # exclusive
        # Empty starts at start_block_num; a committed cursor resumes right after its block.
        start_cursor=os.environ.get("GRAPHOPS_CURSOR", ""),
        # Production mode processes the range in parallel and sends only the output module.
        production_mode=True,
        # Only irreversible blocks, so the server never sends a BlockUndoSignal (reorg rewind).
        final_blocks_only=True,
    )
    # GraphOps authenticates every gRPC call with the x-api-key metadata header.
    stream = service_pb2_grpc.StreamStub(channel).Blocks(
        request, metadata=[("x-api-key", api_key)]
    )
    for response in stream:
        # Skip session, progress and other non-data messages.
        if not response.HasField("block_scoped_data"):
            continue
        data = response.block_scoped_data
        events = json_format.MessageToDict(
            Events.FromString(data.output.map_output.value)
        )
        if "events" not in events:
            continue  # no matching transfers in this block

        # Store the events and this cursor together; after a restart, pass the last stored
        # cursor as start_cursor to continue from the next block without gaps or duplicates.
        record = {"block": str(data.clock.number), "cursor": data.cursor, "data": events}
        print(json.dumps(record))

```

Output

```json
{
  "block": "18000002",
  "cursor": "89c_0WDIRTi08Zu_7v-ibqWwLpc_DFtnVQvlKhFDj46k-XaW3pT3VGYgPU6Cxqn03kPtSQuoj9jLR3sqoJUFtNbryr1m7SZsEHIkwtzv_eG9e6XwMQNJc7lkWLyPZtnRWjTXYgr7erMK5YfuPqDabhNmZZJzfzTljztT9INWIadAvnVmxm2vJpra1_qf9IBB-LZzTezwwCqlUmd-ek1SNJ6AZfSY6T0kZA==",
  "data": {
    "clock": {
      "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141",
      "number": "18000002",
      "timestamp": "2023-08-26T16:21:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAA5nO9oZpSnZ/xOu3a4OCMD9qLVIg=",
            "AAAAAAAAAAAAAAAAiOagwt3Sb+62TwOaLEEpb8s/VkA="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEATMsA=",
          "index": 1,
          "blockIndex": 172,
          "ordinal": "2582"
        },
        "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAApAuxxH9t0nFCor18k7+pjbnR9cU=",
            "AAAAAAAAAAAAAAAAtOFtAWjlLTXKzSxhhbRCgewoydw="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABE0jl0=",
          "index": 2,
          "blockIndex": 315,
          "ordinal": "5139"
        },
        "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db"
      }
    ]
  }
}
{
  "block": "18000003",
  "cursor": "n-jOoTkc7lESaOaRPLPCC6WwLpc_DFtnVQvlKhFCj4L2o3LDjJTwUTVzbkmFkqj3jkDuQ1yuj9bPRXwv9sUBtIfuwe5tvHNuTi5_ko66_ua5Kvv3b1xPIrxqXOzdM9zRWjTXYgr7erIK6dW0OvWIbhRjNsEgeDOxjjgD94BcdqFAsHFkwWj5dp7ahv-Up4sQrbQtEbegkn-mVWMvJEoMa5jRYPqcuW9xYQ==",
  "data": {
    "clock": {
      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",
      "number": "18000003",
      "timestamp": "2023-08-26T16:22:11Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAGAzUMDWC00ilT8sENtVmXwgqI9E=",
            "AAAAAAAAAAAAAAAAqdHgjHeTr2fp2S/jCNVpf7gdPkM="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKrQo0A=",
          "blockIndex": 222,
          "ordinal": "4219"
        },
        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAHHJ6Veo8EbCrfTo2H+DzxHzm3l0=",
            "AAAAAAAAAAAAAAAAw9zAiK70Nz5mWwD8i+RaS2pLJKY="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGvNo=",
          "blockIndex": 230,
          "ordinal": "4543"
        },
        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"
      }
    ]
  }
}
{
  "block": "18000004",
  "cursor": "egpIfwVCY1AAna0iQWwoTaWwLpc_DFtnVQvlKhFFj4yg93rN1M6lBmlzPBqGla_wiUHtQlr9i43PRX97oMRZ6dK7xL5s6SU9Qnh_xY7mqOfvLKWnPQwTdu5gVuKLNtrRWjTXYgr7erUK54PgMvvQNEE0asFyKzC2iT8E9oNdcPJE63Fkwjyvd8aH06qR94pF--chR7f3kiPwVDUpehpeO8SFMvCWtzl0Zw==",
  "data": {
    "clock": {
      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",
      "number": "18000004",
      "timestamp": "2023-08-26T16:22:23Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
            "AAAAAAAAAAAAAAAAQ5VHhZgMUHaDc9wdr9e6OiBSA28="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6NQzqm4=",
          "index": 1,
          "blockIndex": 191,
          "ordinal": "3060"
        },
        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"
      }
    ]
  }
}
{
  "block": "18000005",
  "cursor": "ZLBOgvAzsxIgZXZxiKsc4aWwLpc_DFtnVQvlKhFEj4r09yHDj5ShCWV9bxmEl6nx3BLsSwivj47PRX4q9MRQ6dC-kbpk7HI6En0rlN3u-ubtKvCgOANOduo2W-iJMd_RWjTXYgr7erQK4dfgafWLbkU7Zs8hKDK0jz5RpYJUIqBA6HFkw237d8-H0a_E84JArOBxQuOmwSuiVTcvLx1bNJmFNqabvTtzYg==",
  "data": {
    "clock": {
      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",
      "number": "18000005",
      "timestamp": "2023-08-26T16:22:35Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAyIDYvk6U9kTx2qfTVYtQ/1ocLbc=",
            "AAAAAAAAAAAAAAAAri1GF8hiMJo9daD/s1jHpQCcZz8="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALpDt0AA=",
          "blockIndex": 64,
          "ordinal": "1110"
        },
        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAuG/F6YX6zcrmA0vu/Gy9zYu9jLQ=",
            "AAAAAAAAAAAAAAAAJ/cTre/HCEadgmq49bHkZy8aBw4="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFloLwA=",
          "blockIndex": 203,
          "ordinal": "3669"
        },
        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"
      }
    ]
  }
}
{
  "block": "18000007",
  "cursor": "JtpB3QUXuAcCSUDww-4QDqWwLpc_DFtnVQvlKhFGj9-k9nGQ35v0BmRwOUnTkf323UfpSl2vjd_PFXx888hQ6YXuxepm5SQ4QC0rm4y_-bLneqbyPAJIIL1jDrzaMdnRWjTXYgr7erYKtIfhOabbYRA0Z8J3eGWy2zlQ8IdVd6BCuXE0wTv8e8-HhP-Qo4BJ-uIjEuOpkHqhAT1_eU9fNZ_TYfPO6WhzZA==",
  "data": {
    "clock": {
      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",
      "number": "18000007",
      "timestamp": "2023-08-26T16:22:59Z"
    },
    "events": [
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAtOrojRZD4XJRjd4O8hPCBih5jGU=",
            "AAAAAAAAAAAAAAAAOMWk8i4uKQupu7nj2ar4WC959NE="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa3iEA=",
          "blockIndex": 94,
          "ordinal": "1448"
        },
        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"
      },
      {
        "log": {
          "address": "oLhpkcYhizbB0Z1KLp6wzjYG60g=",
          "topics": [
            "3fJSrRviyJtpwrBo/DeNqpUrp/FjxKEWKPVaTfUjs+8=",
            "AAAAAAAAAAAAAAAAV5dSz/j+568JRGshM+4vn/EMT78=",
            "AAAAAAAAAAAAAAAAFxWj5KFC2LaYExEImVF083rroQ0="
          ],
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0CIc0=",
          "blockIndex": 179,
          "ordinal": "3398"
        },
        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"
      }
    ]
  }
}
```

Historical block: 18,000,002

Commit this cursor with the block’s events; resume from it after a disconnect

Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON

ERC-20 Transfer signature

Sender address, left-padded to 32 bytes

Recipient address, left-padded to 32 bytes

Transfer amount: 1,075,000,000 base units, 1,075 USDC

Transaction containing this log

The CLI recording resumes after block `18000002`. The other recordings show the programs’ default range without `GRAPHOPS_CURSOR`; your own cursor changes their output.

## Check the resume

After a restart, the first block the stream sends should be the block after the last one you
committed. If it is earlier, the consumer saved the cursor before its output or did not save the
latest cursor; if it is later, a cursor was saved for output that never committed.

## Make replays safe

* Save the cursor after the output commits, never before.
* Make writes idempotent. A retry can replay data from the last committed cursor.
* Store the endpoint, package reference, and module name with the cursor, so you resume the same
stream that produced it.
* Handle `BlockUndoSignal` separately from normal output; see
[Cursors and reorgs](/substreams/concepts/cursors-and-reorgs).
* Reconnect with exponential backoff and jitter after retryable disconnects.

## Next steps

* [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs): Handle undo signals with the same cursor discipline.
* [Run a sink](/substreams/guides/run-a-sink): Let a standard sink store cursors for you.
* [Troubleshooting](/substreams/troubleshooting): Fix duplicate data and repeated disconnects.