# Choose a data service Source: https://docs.graphops.xyz/choose-a-service Pick the service that returns the data your application needs in the shape it can use, with the least state for you to maintain. Start from the job, check what each service leaves to you, then run that service's quickstart. ## Match your need to a service | If you need to… | Start with | Why | | --- | --- | --- | | Read contract state or balances now | [RPC](/rpc) | It exposes native EVM node methods without an indexing layer. | | Submit transactions | [RPC](/rpc) | Transaction submission is part of the JSON-RPC interface. | | Query an existing entity schema through GraphQL | [Subgraphs](/subgraphs) | Query indexed entities without building the indexing pipeline yourself. | | Process every block or event in order | [Substreams](/substreams) | It streams deterministic package output with cursors and undo signals. | | Backfill a stream into your own database | [Substreams](/substreams) | Your consumer can replay ranges and materialize the result it needs. | ## What GraphOps runs and what you own _Every service shares organization API keys, metering and credits. The difference is how much state you keep: RPC leaves composition and history to you, Subgraphs keeps an indexed view for you, and Substreams hands you an ordered stream to store yourself._ | Service | GraphOps runs | You own | | --- | --- | --- | | RPC | Nodes behind authenticated endpoints; HTTPS and WebSocket transports | Call composition and retries; Block references for related reads; Any history you index | | Subgraphs | GraphQL endpoints and indexing; Managed Subgraphs and Endpoint Aliases | The Subgraph you select; Queries and page sizes; Upgrade timing | | Substreams | gRPC stream endpoints; Package execution, including history | Package choice and version; Cursor persistence and undo handling; Your destination database | ## How the services differ | Concern | RPC | Subgraphs | Substreams | | ------------------ | ------------------------------------------ | ---------------------------------------------- | -------------------------------------------------------- | | Interface | EVM JSON-RPC over HTTPS and WebSocket. | GraphQL over HTTPS. | gRPC streams with cursors. | | Data shape | Node request and response objects. | GraphQL entities defined by a Subgraph schema. | Block-scoped module output. | | Freshness | Chain head and explicit block references. | Subgraph indexing progress. | Selected start and stop blocks and finality mode. | | Long-running state | Your application polls or subscribes. | GraphOps keeps the indexed Subgraph view. | Your consumer materializes state. | | Recovery | Retry against an explicit block reference. | Inspect `_meta` and indexing status. | Resume from a committed cursor and process undo signals. | ## Subgraphs or Substreams Both give you indexed onchain data, but they leave different work to you. Choose Subgraphs when your application queries entities over GraphQL and you want GraphOps to keep the indexed view; you check its freshness with `_meta`. Choose Substreams when your consumer must process every relevant block in order and store the result in your own database; you persist the cursor and handle undo signals. ## Combine services One product often uses more than one service. Each needs its own API key, and all of them draw from your organization's credit balance. * **Wallet or dapp:** Subgraphs for read-heavy views such as balances and history, and RPC to submit transactions and read the latest contract state. * **Analytics backend:** Substreams to backfill and stream events into your database, and RPC for occasional point reads at a specific block. * **Trading or monitoring bot:** RPC WebSocket subscriptions for new blocks and logs, with a Subgraph for indexed context. When a workload could fit more than one service, start with the smallest interface that completes the job: RPC for direct node calls, Subgraphs for an existing GraphQL entity model, and Substreams when you need an ordered data pipeline. If you are still unsure, [ask GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) with a short description of the workload. ## Discover the APIs from code Tools and agents can read the [API catalog](https://graphops.xyz/.well-known/api-catalog), a standard [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) list of each service's endpoint, machine-readable description, and documentation. RPC and Subgraphs each serve an OpenAPI document at `/openapi.json` on their endpoint origin, such as `https://rpc.graphops.xyz/openapi.json`. Substreams links the gRPC service definitions of the [API versions](/substreams/api#api-versions) it serves. ## Next steps * [Start building](/get-started): Create your account and API key before running a quickstart. * [Make an RPC request](/rpc/getting-started): Read the latest Ethereum block number. * [Query a Subgraph](/subgraphs/getting-started): Read indexed entities from an existing Subgraph. * [Stream five blocks](/substreams/getting-started): Run a standard package through the Substreams CLI. --- # Start building Source: https://docs.graphops.xyz/get-started Go from a new GraphOps account to one authenticated request, query, or stream. You need an email address, or a Google or GitHub account when those sign-in options are available. If you have not picked a service yet, [choose a data service](/choose-a-service) first. _Your application sends requests with an organization API key for one product. GraphOps runs the service against the network you choose, and usage draws credits from your organization's balance._ ## Set up and send your first request ### Create or access an account \[#create-or-access-an-account] [Create account](https://platform.graphops.xyz/auth/register) [Sign in](https://platform.graphops.xyz/auth/start?mode=login) Choose email or an available Google or GitHub option and complete any requested email verification. Then create an organization or select one you already belong to. If a teammate invited you, sign in with the invited email and accept the invitation instead of creating a new organization. If you need help signing in or finding a verification email, see [Secure your account](/platform/account-and-security). ### Check your credits \[#check-your-credits] Open [Billing](https://platform.graphops.xyz/organizations/current/billing) to see your organization's available credits and plan. Every service draws from this one balance. Choose your monthly credits or buy a top-up if your workload needs more; see [Billing and credits](/platform/billing-and-credits) for credit sources and current prices. ### Create a product API key \[#create-a-product-api-key] [Open API keys](https://platform.graphops.xyz/organizations/current/api-keys) Choose the product you plan to use and give the key a label that names the workload, such as `local-dev`. Copy the key when it is shown and store it in a secret manager or server-side runtime configuration; it is not shown again. ### Run one quickstart \[#run-one-quickstart] - [Make an RPC request](/rpc/getting-started): Read the latest Ethereum block number. - [Query a Subgraph](/subgraphs/getting-started): Read indexed entities from an existing Subgraph. - [Stream five blocks](/substreams/getting-started): Run a standard package through the Substreams CLI. Each quickstart shows the expected result. An authentication error usually means the key is missing from the request, formatted incorrectly, or created for a different product; the service's troubleshooting page lists the checks. Examples include the setup steps for each client and a note with the versions used for testing. Use **Example project** to download the complete program with its dependency lockfiles. Set `GRAPHOPS_API_KEY` to your API key before running it. ## Next steps * [Production readiness](/production-readiness): Review credentials, retries, observability, and recovery before launch. * [API keys](/platform/api-keys): Create separate keys for each environment and plan rotation. * [Team and roles](/platform/team-and-roles): Invite teammates with the access their work needs. * [Usage and limits](/platform/usage-and-limits): See which workloads use your credits. --- # Build onchain data products Source: https://docs.graphops.xyz/ [GraphOps](https://graphops.xyz/company) runs onchain data infrastructure so you can read chain state with RPC, query indexed entities with Subgraphs, or stream ordered blocks with Substreams. Pick the interface your application needs, then use one organization to manage access, billing, and usage for every service you add. ## From account to working integration Four steps take you from a new account to your first response. 1. [Create an account](https://platform.graphops.xyz/auth/register): register, verify your email, and create or join an organization. 2. [Check your credits](https://platform.graphops.xyz/organizations/current/billing): see your balance, then choose monthly credits or a top-up that covers your workload. 3. [Create an API key](https://platform.graphops.xyz/organizations/current/api-keys): create a key for one service and store it in your secret manager. 4. [Run a quickstart](/get-started#run-one-quickstart): read a block, query entities, or start your first stream. [Compare RPC, Subgraphs, and Substreams on Choose a data service.](/choose-a-service) ## Next steps * [Start building](/get-started): Set up your account, credits, and API key, then run your first quickstart. * [Choose a data service](/choose-a-service): Compare what each service returns, what GraphOps runs, and what you own. * [Manage your organization](/platform): Protect API keys, plan spending, and give your team the right access. --- # Production readiness Source: https://docs.graphops.xyz/production-readiness Launch with credentials you can replace, data you can trust, and failures you can diagnose and recover from. Work through this checklist with your team after your first successful request, then follow your service's usage and troubleshooting pages for protocol-specific decisions. _Six areas to check before production traffic, in the order of this page. Each section below lists the full checklist for its area._ - Credentials and access: A separate key per product and environment. - Billing, credits, and limits: Monthly credits sized and automatic top-ups on. - Data correctness: Block references, _meta and committed cursors. - Timeouts and retries: Bounded backoff with a retry budget. - Observability: Enough context to diagnose, never secrets. - Recovery: Tested reconnects, rotation and support path. ## Credentials and access * Create a separate API key for each product, environment, and workload, with a label that names it. * Store keys in a secret manager or server-side runtime configuration. * Keep keys out of logs, analytics events, browser bundles, support messages, and source control. * Confirm each production key belongs to the intended organization and product. * Decide who can rotate and revoke production keys, and test a rotation before launch. See [API keys](/platform/api-keys) and the authentication page for your service. ## Billing, credits, and limits * Size your [monthly credits](/platform/billing-and-credits#choose-your-monthly-credits) to cover launch traffic. * Turn on [automatic top-ups](/platform/billing-and-credits#automatic-top-ups) with a spending limit you are comfortable with, so a traffic spike adds credits instead of pausing your services. At zero or below, requests pause. * Set [low-credit alerts](/platform/billing-and-credits#get-notified-before-credits-run-low) so the right people hear before the balance runs out. * Check your [organization limits](/platform/billing-and-credits#organization-limits) for API keys and Managed Subgraphs, and ask support for an increase before you need it. * Read your service's usage page before choosing concurrency, batch size, or backfill strategy. * Treat access (`403`) and limit responses as configuration or billing problems; do not retry them indefinitely. [Open billing](https://platform.graphops.xyz/organizations/current/billing) ## Data correctness Decide how fresh and how stable each result must be, then use the service's tool for it. | Service | Correctness decision | | ---------- | --------------------------------------------------------------------------------------------------- | | RPC | Anchor multi-request workflows to an explicit block number, hash, `safe`, or `finalized` reference. | | Subgraphs | Inspect `_meta` when indexing freshness or indexing errors affect the result. | | Substreams | Persist cursors only after committing output, and handle undo signals for reversible data. | ## Timeouts and retries _Retry only failures that can succeed unchanged, with bounded backoff, jitter and a retry budget. Authentication, access and validation failures need a configuration, billing or request fix first. A Substreams consumer resumes from its last committed cursor instead._ Retry with backoff and jitter: - Timeouts and dropped connections - 429 Too Many Requests - 502, 503 and 504 Fix before sending again: - 401: missing or invalid API key - 403: organization access paused, such as when credits run out - Malformed or invalid requests * Set explicit connection and request timeouts. * Retry transient failures with bounded exponential backoff and jitter. * Respect server retry guidance when a response supplies it. * Give each workflow a retry budget so a downstream failure cannot create unbounded traffic. Successful retries can count as additional usage. ## Observability Record enough context to diagnose a failed request without recording secrets. One structured entry per failed attempt is enough to answer most support questions: A reference template for a structured log entry for a failed RPC request. It identifies the organization, key, network and attempt without recording the key itself. Use the same fields for any service. ```json { "time": "2026-10-21T14:03:12Z", "service": "rpc", "organization": "acme-production", "apiKeyLabel": "production-indexer", "endpoint": "/v1/evm/1", "status": 429, "error": "Too Many Requests", "requestId": "", "attempt": 2 } ``` - UTC timestamp - GraphOps service - Organization name or ID - Key label, never the key - Network or endpoint path - HTTP or gRPC status - Safe error detail - When the response supplies one - Retries used from the budget For Subgraphs, record the Endpoint Alias or Deployment CID and `_meta.block.number`. For Substreams, record the package, module, and last committed cursor or block when it is safe to share. ## Recovery * Define what each client does after a disconnect or partial workflow failure. * Make writes and materialization steps idempotent wherever a retry can repeat work. * Test the troubleshooting path for each service you use. * Decide who contacts [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) and which sanitized details they provide. ## Next steps * [RPC usage and limits](/rpc/usage-and-limits): Plan requests, batches, WebSockets, and retries. * [Subgraphs usage and limits](/subgraphs/usage-and-limits): Plan query complexity, indexing checks, and retries. * [Substreams usage and limits](/substreams/usage-and-limits): Plan streams, backfills, cursors, and output volume. * [Support and status](/platform/support-and-status): Know what to include when you need help. --- # API integration Source: https://docs.graphops.xyz/subgraphs/api Use this reference to build Subgraph requests and interpret their responses. GraphOps Subgraphs exposes standard GraphQL query endpoints over HTTPS. Each endpoint is selected by Deployment CID, Graph Network Subgraph ID, or GraphOps Endpoint Alias, and authenticated with a Subgraphs API key. ## Endpoint patterns Header-authenticated endpoints: ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ https://subgraphs.graphops.xyz/v1/subgraphId/ https://subgraphs.graphops.xyz/v1/alias/ ``` _A Subgraphs endpoint names what to query in its path: the identifier type, then the identifier. This example uses an Endpoint Alias; deploymentCID and subgraphId work the same way. Send the API key in the Authorization header or append it to the path._ ```text https://subgraphs.graphops.xyz/v1/alias/ Authorization: Bearer ``` - Subgraphs host: `https://subgraphs.graphops.xyz` - Identifier type: `alias` - Identifier: `` Route-token endpoints: ```text https://subgraphs.graphops.xyz/v1/deploymentCID// https://subgraphs.graphops.xyz/v1/subgraphId// https://subgraphs.graphops.xyz/v1/alias// ``` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ Authorization: Bearer ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=subgraphs) [Supported networks](/subgraphs/networks) See [Authentication](./authentication) to choose how to send your API key and protect it in logs. ## Request envelope Subgraph queries use the standard GraphQL HTTP request shape: | Field | Type | Required | Notes | | --------------- | ------ | -------- | ----------------------------------------------------------------------------- | | `query` | string | Yes | GraphQL query or mutation text. Query support depends on the Subgraph schema. | | `variables` | object | No | Variables referenced by the query. | | `operationName` | string | No | Required only when the document contains multiple named operations. | ```json { "query": "query Pools($first: Int!) { pools(first: $first) { id } }", "variables": { "first": 5 }, "operationName": "Pools" } ``` Output ```json { "data": { "pools": [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9" }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505" }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781" }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28" }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79" } ] } } ``` Present when the query ran No matching entities, not an error ## Request These clients send the request envelope above through an Endpoint Alias. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with your Endpoint Alias. [Example project](/examples/subgraphs.api.sh-4.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/alias/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query Pools($first: Int!) { pools(first: $first) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } }", "variables": { "first": 5 }, "operationName": "Pools" }' ``` Output ```json { "data": { "pools": [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } } ], "_meta": { "block": { "number": 26285619, "hash": "0x09fffa722f278b60998290f6534850d5b2248e083ada05d332aa684f24f8302e" } } } } ``` Present when the query ran No matching entities, not an error Indexed block behind the result Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/subgraphs.api.ts-5.tar.gz) ```typescript export {}; const response = await fetch( "https://subgraphs.graphops.xyz/v1/alias/", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: ` query Pools($first: Int!) { pools(first: $first) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } `, variables: { first: 5 }, operationName: "Pools", }), }, ); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); console.log(JSON.stringify(result.data.pools)); ``` Output ```json [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } } ] ``` No matching entities, not an error Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` with your Endpoint Alias. Tested with Go 1.26.0. [Example project](/examples/subgraphs.api.go-8.tar.gz) ```go package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { payload, err := json.Marshal(struct { Query string `json:"query"` Variables map[string]any `json:"variables"` OperationName string `json:"operationName"` }{ Query: ` query Pools($first: Int!) { pools(first: $first) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } `, Variables: map[string]any{"first": 5}, OperationName: "Pools", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, "https://subgraphs.graphops.xyz/v1/alias/", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } var result struct { Data struct { Pools json.RawMessage `json:"pools"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { log.Fatal(err) } var pools bytes.Buffer if err := json.Compact(&pools, result.Data.Pools); err != nil { log.Fatal(err) } fmt.Println(pools.String()) } ``` Output ```json [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } } ] ``` No matching entities, not an error Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/subgraphs.api.rust-9.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { let response = reqwest::Client::new() .post("https://subgraphs.graphops.xyz/v1/alias/") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "query": r#" query Pools($first: Int!) { pools(first: $first) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } "#, "variables": { "first": 5 }, "operationName": "Pools", })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; println!("{}", result["data"]["pools"]); Ok(()) } ``` Output ```json [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } } ] ``` No matching entities, not an error Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/subgraphs.api.python-7.tar.gz) ```python import json import os import requests response = requests.post( "https://subgraphs.graphops.xyz/v1/alias/", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "query": """ query Pools($first: Int!) { pools(first: $first) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } """, "variables": {"first": 5}, "operationName": "Pools", }, timeout=30, ) response.raise_for_status() print(json.dumps(response.json()["data"]["pools"])) ``` Output ```json [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } } ] ``` No matching entities, not an error ## Response Successful GraphQL responses include `data`. An empty list is a valid result when no entities match: ```json { "query": "query EmptyPools { pools(first: 10, where: { id_gt: \"0xffffffffffffffffffffffffffffffffffffffff\" }, orderBy: id) { id } _meta { block { number } } }" } ``` Output ```json { "data": { "pools": [], "_meta": { "block": { "number": 26285631 } } } } ``` A successful query with no matching pools Historical block served: 26,285,631 Responses can include `errors` even when the HTTP request succeeds. Check for `errors` on every response: ```json { "query": "query InvalidField { notARealField }" } ``` Output ```json { "errors": [ { "message": "Cannot query field \"notARealField\" on type \"Query\".", "locations": [ { "line": 1, "column": 22 } ] } ] } ``` Can arrive with HTTP 200 Fix the query; retrying will not help ## Response headers | Header | Value | Present when | | ------------------ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `X-Deployment-CID` | The Deployment CID that served the request, for example `QmRYcnVddpADmnZz812rc1WDE3n2TRsuP5PcGsMxZjaZMM` | Every response returned by a deployment, on all three endpoint patterns, including responses with GraphQL `errors` or a non-`2xx` status | `X-Deployment-CID` tells you which deployment answered without changing your query. It matches `_meta.deployment` for the same request. An Endpoint Alias or Graph Network Subgraph ID can serve a different deployment over time: for example, after a promotion, while a new version is still being indexed, or when an Endpoint Alias falls back to a previous version. Record the header with each response when you need to know which version produced a result. * The header is absent when GraphOps rejects the request before it reaches a deployment, such as an invalid API key or an unknown Endpoint Alias. * For a Deployment CID endpoint, the value is the CID in the endpoint path. * Browser clients can read the header: responses list it in `Access-Control-Expose-Headers`. * Header names are case-insensitive; some HTTP clients report it as `x-deployment-cid`. ## Error responses | Code or status | Meaning | Typical response | | ---------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------- | | `400` | Invalid HTTP request, invalid JSON, or malformed GraphQL document. | Fix request encoding or query syntax. | | `401` | Missing or invalid API key. | Check [Authentication](./authentication). | | `403` | Service access for the organization is paused, usually because credits ran out. | Check your credit balance and service access in Billing. | | `404` | Deployment CID, Graph Network Subgraph ID, or Endpoint Alias was not found. | Confirm the identifier in the platform UI. | | `413` | Request or response is too large. | Reduce query selection or page size. | | `429` | Organization exceeded query rate limits. | Back off and reduce request rate. | | `5xx` | Gateway or upstream indexing service failure. | Retry if transient; contact support if persistent. | | GraphQL `errors` | Schema, validation, resolver, or indexing issue. | Inspect the error message and `_meta`. | ## Next steps * [Authentication](./authentication): Choose header or route-token authentication and protect your key. * [Query a Subgraph](./guides/query-a-subgraph): Send a complete query from curl, TypeScript, or Python. * [Identifiers and Endpoint Aliases](./concepts/identifiers-and-aliases): Choose the identifier your endpoint uses. --- # Authentication Source: https://docs.graphops.xyz/subgraphs/authentication Authenticate every Subgraph query with an organization-scoped API key, sent either in the `Authorization` header or in the endpoint path. Use one method per request, and prefer the header when your client can set one. ## Create and rotate API keys Create and rotate Subgraphs API keys in the GraphOps Platform. The platform only shows the plaintext key at creation or rotation time, so store it in your secret manager immediately. Each key belongs to one organization and product. Use separate keys for production, staging, and local development so you can replace one environment's credentials without affecting the others. [Create or rotate a Subgraphs key](https://platform.graphops.xyz/organizations/current/api-keys) ## Authentication methods | Method | Endpoint or header | Use it for | | -------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Header authentication | `Authorization: Bearer ` | Server-side clients and backends that can attach headers. | | Route-token authentication | `https://subgraphs.graphops.xyz/v1/deploymentCID//` | Query one exact deployment from a client that cannot attach headers. | | Route-token authentication | `https://subgraphs.graphops.xyz/v1/subgraphId//` | Follow a Graph Network Subgraph ID from a client that cannot attach headers. | | Route-token authentication | `https://subgraphs.graphops.xyz/v1/alias//` | Query an Endpoint Alias from a client that cannot attach headers. | Prefer header authentication when your client can set headers. Use route-token authentication when a client cannot attach `Authorization`, such as a hosted dashboard tool that accepts only a URL. When you query an Endpoint Alias, the API key also selects the organization whose Endpoint Alias namespace is used. An Endpoint Alias name resolves only for keys from the organization that owns it. *With header authentication the key travels only in the Authorization header. With route-token authentication it is part of the URL, and every system that records the URL records the key, so redact the path after the identifier.* - Header authentication: `https://subgraphs.graphops.xyz/v1/alias/` and `Authorization: Bearer `. The URL holds no secret. - Route-token authentication: `https://subgraphs.graphops.xyz/v1/alias//`. The URL is the secret. - Full URLs are often copied to: proxy logs, browser history, metrics labels, crash reports. ## Header authentication Send the API key as a bearer token: ```http Authorization: Bearer ``` Example query endpoint: ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ ``` Use header authentication for server-side HTTPS clients, backend workers, and GraphQL clients that support custom request headers. ## Route-token authentication Put the API key after the Subgraph identifier: ```text https://subgraphs.graphops.xyz/v1/deploymentCID// https://subgraphs.graphops.xyz/v1/subgraphId// https://subgraphs.graphops.xyz/v1/alias// ``` Use route-token authentication for clients that cannot attach custom headers. Do not send the same API key in both the URL and the `Authorization` header. > **Treat route-token URLs as secrets** > > Route-token authentication puts the API key in the URL. URLs can appear in application logs, > reverse-proxy logs, browser history, crash reports, metrics labels, and support screenshots. Redact > the path after the Deployment CID, Graph Network Subgraph ID, or Endpoint Alias, and rotate the > key if a route-token URL is exposed. ## Security guidance * Store API keys in a server-side secret manager when possible. * Keep organization API keys out of public frontend bundles; send requests through your backend. * Use separate keys per environment. A route-token URL exposes the key to anyone who can read it. * Redact API keys from URLs before logging request paths, errors, or metrics. * Rotate keys from the platform when a key or route-token URL is exposed. ## Fix `401 Unauthorized` A `401` means the key is missing, malformed, or no longer valid. Work through the checks in [Troubleshooting](./troubleshooting#401-unauthorized). ## Next steps * [Query your first Subgraph](./getting-started): Send an authenticated query and read the response. * [API integration](./api): Check endpoint shapes, the request envelope, and error responses. * [Identifiers and Endpoint Aliases](./concepts/identifiers-and-aliases): Learn how the key selects your organization's Endpoint Aliases. --- # Choose your Subgraph setup Source: https://docs.graphops.xyz/subgraphs/choose-a-setup Pick a setup that keeps your queries working when a Subgraph changes. Answer three questions below, then follow the recommended setup. You can change most choices later without changing your application's URL. ## Know the pieces You need five terms to make this choice: * **A Subgraph** turns blockchain data into entities you query with GraphQL. It changes over time through new versions. * **A version**, or deployment, is identified by its Deployment CID, a value starting with `Qm`. A version has no data until it is indexed, which takes time after it is released. * **The Graph Network** is where Subgraph owners publish versions under a Graph Network Subgraph ID. That ID moves to each new version as soon as the owner publishes it, indexed or not. * **A Managed Subgraph** has GraphOps keep a Subgraph indexed for your organization. It prepares each new version alongside the one serving queries and switches only when the new one is ready. * **An Endpoint Alias** is a query URL your organization names, such as `/v1/alias/orders`. You choose what it points to and can change that without changing your application. [Identifiers and Endpoint Aliases](./concepts/identifiers-and-aliases) explains each piece in detail. ## Answer three questions 1. **Whose Subgraph is it?** Someone else's, already published on The Graph Network, or your own. 2. **Should GraphOps keep it indexed?** A Managed Subgraph has GraphOps index it for your organization and prepare each new version before your application moves, with indexing charges. Without one, indexers on The Graph Network serve it at the standard query rate, while an indexer serves the version you query. Managed indexing is available only on its [supported networks](./networks#managed-indexing). 3. **When a new version appears, what should happen?** * Your application moves to it automatically, but only once it is indexed. * Your team reviews and tests each version before your application moves. * Your application moves to it once indexers on The Graph Network serve it. * Your application stays on one version until you change it. | Your answers | Recommended setup | | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Someone else's Subgraph, no managed indexing, move once indexers serve it | [Follow the latest version](#follow-the-latest-version) | | Someone else's Subgraph, no managed indexing, stay on one version | [Approve each upgrade](#approve-each-upgrade), without managed indexing | | Someone else's Subgraph, managed indexing, move automatically once indexed | [Follow the owner's versions safely](#follow-the-owners-versions-safely) | | Someone else's Subgraph, managed indexing, review each version | [Approve each upgrade](#approve-each-upgrade) | | Your own Subgraph, managed indexing, release from the platform | [Develop and release your own Subgraph](#develop-and-release-your-own-subgraph) | | Your own Subgraph, managed indexing, release by publishing on The Graph Network | [Release by publishing](#release-by-publishing) | | Your own Subgraph, no managed indexing | Publish it on The Graph Network, then [follow the latest version](#follow-the-latest-version) or pin one | Setups with a Managed Subgraph need an Owner or Developer role, available credits, room in your plan and a deployment on a supported network. See [Start indexing](./managed-subgraphs#start-indexing) for the full requirements. If you are an Owner or Developer, the platform can ask these questions for you. Open **Managed Subgraphs** and choose **Set up a Subgraph**, or choose it from the actions menu while you [explore a Subgraph](./guides/discover-public-subgraphs#keep-the-subgraph-in-your-organization). It asks the same three questions, each linked to the part of this guide that explains it, and offers only the choices that apply to your Subgraph and its network. It then shows what the setup will create, what it costs and the query URL your application will use, and creates it. The page address records your answers, so you can share it or reload it without starting over. ## Follow the latest version Best when your application should follow the owner's newest version without indexing charges. 1. Find the Subgraph in [Discover Subgraphs](./guides/discover-public-subgraphs). 2. Create an Endpoint Alias with a **Graph Network Subgraph ID** target and the **Latest available version** target behavior, and use its URL in your application. When the owner publishes a new version, your Endpoint Alias keeps serving the previous version until an indexer on The Graph Network serves the new one healthy and close to the chain head, then moves to it. A schema change from the owner still reaches your application with the new version. Queries use the standard rate, with no indexing charges. The Graph Network Subgraph ID endpoint from **Get query endpoint**, and an Endpoint Alias with the **Latest published version** target behavior, move as soon as the owner publishes instead. Until indexers serve the new version, queries can fail or return incomplete data. If you start from a Deployment CID instead, the endpoint stays on that exact version until you change it, and answers only while an indexer on The Graph Network serves that version. ## Follow the owner's versions safely Best for production applications that should stay current without an indexing gap. 1. Create a Managed Subgraph with a **Graph Network Subgraph ID** target. 2. Create an Endpoint Alias with a **Managed Subgraph** target and the **Current version** target behavior, and use its URL in your application. GraphOps indexes each new version as Next while the current one keeps serving, then promotes it when it is ready. Your Endpoint Alias moves at promotion. Promotion doesn't wait for your approval, so a breaking schema change from the owner still reaches your application; watch the Subgraph's releases and test your queries against the Next version. The Managed Subgraph accrues indexing charges, for both versions during an upgrade, and uses one of your plan's Managed Subgraph slots. Queries use the managed rate. ## Approve each upgrade Best for production applications that must not change without testing. 1. Create a Managed Subgraph with a **Deployment CID** target, using the version you have tested. 2. Create an Endpoint Alias with a **Managed Subgraph** target, and use its URL in your application. 3. When the owner publishes a version you want, choose **Start upgrade** with its Deployment CID, test it while it indexes, and choose **Promote version**. Nothing changes until you promote, and GraphOps keeps your version indexed even if indexers on The Graph Network stop serving it. Charges match the previous setup. Without managed indexing, an Endpoint Alias with a **Deployment CID** target also waits for you, at no indexing cost. It depends on indexers on The Graph Network continuing to serve that version, and its move to a new version doesn't wait for indexing, so test the new Deployment CID before you change the target. ## Develop and release your own Subgraph Best while you build a Subgraph and iterate through many versions. 1. Deploy your Subgraph with Graph CLI and create a Managed Subgraph with a **Deployment CID** target from its Deployment CID. 2. Create a production Endpoint Alias with a **Managed Subgraph** target and the **Current version** target behavior. Optionally, create a staging Endpoint Alias on the same Managed Subgraph with the **Next version** target behavior; **Set up a Subgraph** can create both. 3. Stage each new version as Next, query it through the staging Endpoint Alias or its Deployment CID while it indexes, and promote it when you are ready to release. Until you promote, your production URL keeps serving the current version. Each version you stage indexes from its start block and accrues indexing charges while it is staged. [Develop and release a Subgraph](./guides/develop-and-release) walks through iterating, releasing and rolling back. If managed indexing doesn't support your Subgraph's network, publish it on The Graph Network and pin a tested version with an Endpoint Alias, as in [Approve each upgrade](#approve-each-upgrade). ## Release by publishing Best when your team already publishes on The Graph Network and wants that publication to be the release. 1. Create a Managed Subgraph with a **Graph Network Subgraph ID** target for your Subgraph's ID. 2. Create an Endpoint Alias with a **Managed Subgraph** target for your application. Each version you publish is indexed and promoted automatically once it is ready, so test it before you publish. [Develop and release a Subgraph](./guides/develop-and-release#release-by-publishing-on-the-graph-network) shows one way to do that. ## Compare the setups | Setup | Moves to a new version | Waits until it is indexed | You approve each version | Indexing charges | | ----------------------------------------------------------------------------------- | ---------------------- | ------------------------------------- | ------------------------ | ---------------- | | Graph Network Subgraph ID endpoint, or Endpoint Alias with Latest published version | Automatically | No | No | No | | Endpoint Alias with a Graph Network Subgraph ID target and Latest available version | Automatically | Yes, by indexers on The Graph Network | No | No | | Endpoint Alias with a Deployment CID target | When you change it | No | Yes | No | | Endpoint Alias targeting a Managed Subgraph with a Graph Network Subgraph ID target | Automatically | Yes | No | Yes | | Endpoint Alias targeting a Managed Subgraph with a Deployment CID target | When you promote | Yes | Yes | Yes | Every setup that moves automatically can deliver breaking schema changes chosen by the Subgraph owner. Current prices are on [Usage and limits](./usage-and-limits). ## Change your setup later Point your application at an Endpoint Alias from the start, and you can move between setups without an application release: * Retarget the Endpoint Alias with **Change target**. * Switch a Managed Subgraph between approving and following versions with **Change target type**. ## Next steps * [Query your first Subgraph](./getting-started): Create an API key and read entities from an existing deployment. * [Managed Subgraphs](./managed-subgraphs): Start indexing and run your first upgrade. * [Manage Endpoint Aliases](./guides/manage-aliases): Create the stable URL your application uses. --- # Query your first Subgraph Source: https://docs.graphops.xyz/subgraphs/getting-started Query an existing Subgraph and check how recently its data was indexed. This example reads the five largest pools by total value locked from a Uniswap-style schema and includes `_meta` for indexing information. **Expected result:** five pools and the indexed block in `_meta`. Your endpoint can return different pools and values as indexing advances. Use a deployment that exposes `pools`, `token0`, `token1`, and `totalValueLockedUSD`, or adapt the query to [your Subgraph's schema](./schema-reference). Replace `` with its Deployment CID, not a Graph Network Subgraph ID. ## Prerequisites * A GraphOps Platform account. * An organization with access to Subgraphs. * An Owner or Developer role to create a Subgraphs API key, or a key supplied by your team. * A public Deployment CID for a Subgraph with the fields used in this example. ### Create an API key \[#create-an-api-key] [Create a Subgraphs API key](https://platform.graphops.xyz/organizations/current/api-keys) Choose your organization, create the key for Subgraphs, and store the plaintext value in your secret manager. ### Choose the deployment \[#choose-the-deployment] Use the Deployment CID published by the Subgraph owner or returned by your Graph CLI deployment. To find a public dataset and its current Deployment CID, search [Discover Subgraphs](./guides/discover-public-subgraphs). The deployment endpoint stays on that version even if the owner publishes an upgrade. The separate `subgraphId` endpoint follows the owner's upgrades, but moves to each new version before it is indexed. Before you build on either one, see [Choose your Subgraph setup](./choose-a-setup). ### Query the deployment \[#query-the-deployment] Run a GraphQL query against the Deployment CID endpoint. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with the Deployment CID of a Uniswap-style Subgraph. [Example project](/examples/subgraphs.getting-started.sh-1.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/deploymentCID/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query TopPools { pools(first: 5, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } }" }' ``` Output ```json { "data": { "pools": [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191775.1218005724860114722196234" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ], "_meta": { "block": { "number": 26285620, "hash": "0x7931a9fc337c615e67690a3083685a5680a72653718c13451f7697ea856b14f3" }, "hasIndexingErrors": false } } } ``` The entities your query selected Indexing state behind this result Highest indexed block served Check before trusting the data Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with the Deployment CID of a Uniswap-style Subgraph. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/subgraphs.getting-started.ts-2.tar.gz) ```typescript export {}; const response = await fetch( "https://subgraphs.graphops.xyz/v1/deploymentCID/", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: ` query TopPools { pools(first: 5, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } `, }), }, ); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); console.log(JSON.stringify(result.data.pools)); ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191775.1218005724860114722196234" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] ``` Up to five pools, ordered by total value locked at block 18,000,000 Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` with the Deployment CID of a Uniswap-style Subgraph. Tested with Go 1.26.0. [Example project](/examples/subgraphs.getting-started.go-5.tar.gz) ```go package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { payload, err := json.Marshal(struct { Query string `json:"query"` }{ Query: ` query TopPools { pools(first: 5, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } `, }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, "https://subgraphs.graphops.xyz/v1/deploymentCID/", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } var result struct { Data struct { Pools json.RawMessage `json:"pools"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { log.Fatal(err) } var pools bytes.Buffer if err := json.Compact(&pools, result.Data.Pools); err != nil { log.Fatal(err) } fmt.Println(pools.String()) } ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191956.8055480088623983061845949" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] ``` Up to five pools, ordered by total value locked at block 18,000,000 Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with the Deployment CID of a Uniswap-style Subgraph. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/subgraphs.getting-started.rust-6.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { let response = reqwest::Client::new() .post("https://subgraphs.graphops.xyz/v1/deploymentCID/") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "query": r#" query TopPools { pools(first: 5, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } "#, })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; println!("{}", result["data"]["pools"]); Ok(()) } ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191775.1218005724860114722196234" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] ``` Up to five pools, ordered by total value locked at block 18,000,000 Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with the Deployment CID of a Uniswap-style Subgraph. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/subgraphs.getting-started.python-4.tar.gz) ```python import json import os import requests response = requests.post( "https://subgraphs.graphops.xyz/v1/deploymentCID/", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "query": """ query TopPools { pools(first: 5, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } """ }, timeout=30, ) response.raise_for_status() print(json.dumps(response.json()["data"]["pools"])) ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191775.1218005724860114722196234" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] ``` Up to five pools, ordered by total value locked at block 18,000,000 GraphQL reports query problems in an `errors` array, often with HTTP status `200`. If the response has `errors` instead of `data`, compare the field names with the deployment's schema in [Schema reference](./schema-reference). For `401` or `404` responses, see [Troubleshooting](./troubleshooting). ## Next steps * [Manage Endpoint Aliases](./guides/manage-aliases): Give the endpoint a stable name your application can keep across releases. * [Paginate results](./guides/paginate-results): Read a larger result set in bounded pages a worker can resume. * [Managed Subgraphs](./managed-subgraphs): Have GraphOps index your own Subgraph and prepare upgrades. --- # GraphOps Subgraphs Source: https://docs.graphops.xyz/subgraphs Build dashboards, activity views, and application APIs from indexed blockchain entities. GraphOps Subgraphs serves the open Subgraph standard over GraphQL, so you can query an existing deployment on The Graph Network or a Subgraph you publish yourself, and let GraphOps keep your own Subgraph indexed. ## Subgraphs at a glance _New blocks are indexed as they arrive. Your app's GraphQL queries are answered from that index._ - Your app: GraphQL - Subgraphs: Index - Chain: New blocks - [Run the Subgraphs quickstart](/subgraphs/getting-started) - [Open the Subgraphs reference](/subgraphs/api) ## Where to start - [Query an existing Subgraph](/subgraphs/getting-started): Create an API key, choose a Deployment CID, and run your first GraphQL query. - [Choose your setup](/subgraphs/choose-a-setup): Decide what happens to your queries when a new version is released. - [Keep your Subgraph indexed](/subgraphs/managed-subgraphs): Let GraphOps run indexing and promote new versions when they are ready. *Your application sends one GraphQL query. GraphOps answers from the indexed entities and reports the block behind the answer in _meta. The pairs shown are illustrative.* ```graphql pools(first: 3, orderBy: totalValueLockedUSD, orderDirection: desc) { token0 { symbol } token1 { symbol } } _meta { block { number } } ``` - Response: USDC / WETH, WBTC / WETH, USDC / USDT, with _meta.block.number: N. Each Subgraph defines its own schema of entities and relationships. GraphOps answers your query from the latest indexed block and reports that block in `_meta`. Everything after the response belongs to your application: which fields you request, how you page through results, and how you present freshness to users. ## What you can build * **Views that read entities, not node calls.** Show the largest pools, recent swaps, or open positions with one query. See [Query a Subgraph](/subgraphs/guides/query-a-subgraph). * **Exports and sync jobs over large collections.** Walk every entity in bounded pages that a worker can resume. See [Paginate results](/subgraphs/guides/paginate-results). * **Freshness checks with every answer.** Detect indexing lag and errors before stale data reaches users. See [Monitor sync health](/subgraphs/guides/monitor-sync-health). * **Releases behind a stable URL.** Test each new version of your Subgraph, then move your application to it without changing its endpoint. See [Develop and release a Subgraph](/subgraphs/guides/develop-and-release). ## Choose Subgraphs when Use Subgraphs when your application needs entities and their relationships, such as pools, positions, or votes, and an existing GraphQL schema fits or you can build one with Graph CLI. Use another GraphOps service when your job is different: | Need | Use | | --------------------------------------------------------------------- | ------------------------- | | Direct contract reads at an explicit block, or transaction submission | [RPC](/rpc) | | Block-by-block history or an ordered stream into your own storage | [Substreams](/substreams) | ## Choose how you address a Subgraph Every query goes to one of three endpoint shapes. The choice decides who moves your application to a new version: | Identifier | Endpoint shape | Who changes the version | | ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | | Deployment CID | `/v1/deploymentCID/` | No one. It always serves the same deployment. | | Graph Network Subgraph ID | `/v1/subgraphId/` | The Subgraph owner, by publishing on The Graph Network, before the new version is indexed. | | Endpoint Alias | `/v1/alias/` | Your organization, through the Endpoint Alias target. | An Endpoint Alias can also target a Managed Subgraph, which moves only after a new version is indexed and promoted. [Choose your Subgraph setup](/subgraphs/choose-a-setup) recommends a setup from a few questions, and [Identifiers and Endpoint Aliases](/subgraphs/concepts/identifiers-and-aliases) explains what each one does during an upgrade. Query access covers Subgraphs available through The Graph Network, on every network it serves; managed indexing runs on a smaller set. [Supported networks](/subgraphs/networks) lists both. ## Plan for production Before you rely on query results: * Include `_meta` on important reads and design for indexing lag. Recent data can be corrected by a chain reorganization. * Treat a GraphQL `errors` array as a failure even when the HTTP status is `200`. * Use an Endpoint Alias for production clients so a release does not require an application change. Target a Managed Subgraph when new versions should reach clients only once indexed. Queries and managed indexing use the rates on [Usage and limits](/subgraphs/usage-and-limits). ## Next steps * [Query your first Subgraph](/subgraphs/getting-started): Read pools from an existing deployment and check its freshness. * [Managed Subgraphs](/subgraphs/managed-subgraphs): Keep your Subgraph indexed and promote new versions on your schedule. * [Discover Subgraphs](/subgraphs/guides/discover-public-subgraphs): Find a dataset, inspect its schema, and get an endpoint. --- # Managed Subgraphs Source: https://docs.graphops.xyz/subgraphs/managed-subgraphs Keep your Subgraph indexed without running the indexing infrastructure yourself. With Managed Subgraphs, you can monitor progress, prepare a new version while the current one serves queries, and choose manual releases or follow versions published on The Graph Network. Pair a Managed Subgraph with an Endpoint Alias to keep the same application URL across upgrades. If you only need to query an existing deployment, start with [Query your first Subgraph](./getting-started). To decide whether you need managed indexing, see [Choose your Subgraph setup](./choose-a-setup). ## Start indexing Before you start, check that you have: * An Owner or Developer role in the organization. * A monthly plan for the organization. A plan that is set to cancel counts until it ends. See [Managed Subgraphs need a monthly plan](../platform/billing-and-credits#managed-subgraphs-need-a-monthly-plan). * A positive available credit balance. Support can arrange a credit exception when appropriate. * Room under your organization's [Managed Subgraph limit](#check-your-organizations-limit). * A deployment with a valid manifest and query schema that uses one [supported network](./networks) across all data sources and templates. To have the platform recommend a setup and create the Endpoint Alias with it, choose **Set up a Subgraph** instead; see [Choose your Subgraph setup](./choose-a-setup). Otherwise, add the Managed Subgraph directly: 1. Add a Managed Subgraph: [Create Managed Subgraph](https://platform.graphops.xyz/organizations/current/services/subgraphs/managed/new) While you [explore a Subgraph](./guides/discover-public-subgraphs#keep-the-subgraph-in-your-organization), you can instead choose **Create Managed Subgraph** to start with its identifier. 2. On **Target**, choose what it follows: **Deployment CID** for a pinned version that you upgrade manually, or **Graph Network Subgraph ID** to follow future published versions automatically. A Graph Network Subgraph ID resolves its latest published deployment before indexing begins. 3. Enter the identifier. A Deployment CID starts with `Qm` and has 46 characters; a Graph Network Subgraph ID has 43 or 44 characters. The wizard reports an incomplete or malformed identifier before checking anything, then checks the manifest and shows the deployment's network and start block. Fix any reported problem, or retry a check that could not complete from the button beside the field. 4. **Upgrades** states how the target type moves to new versions. Then give the Managed Subgraph a name your team will recognize, review what will be created, and create it. 5. Optionally add an Endpoint Alias that follows this Managed Subgraph, so your application keeps one query URL across upgrades. Name it, then choose which version it serves: **Current version**, selected for you, moves when you promote a version and suits production; **Next version** serves each version you prepare while it indexes, so you can test it before you promote it. See [Choose a target behavior](./guides/manage-aliases#choose-a-target-behavior). Then get its query endpoint. You can skip this step and add an Endpoint Alias later. If the Endpoint Alias can't be created, for example because the name is taken, your Managed Subgraph is still created: retry with another name or skip. ## Know when your data is ready Check indexing progress and health before relying on new data. A Managed Subgraph's **Overview** leads with its status, sync lag, network, estimated hourly cost, Target type and the Deployment CIDs it serves. **Versions** shows each version's indexing progress, and **Endpoint Aliases** lists the Endpoint Aliases linked to it. * **Available** means the current version is healthy and serving queries close to the network head. A small lag can remain, and the page always shows how many blocks it is behind. Check that lag when your application needs the most recent block. * Hover over, select, or tap a status label to read its explanation and open its section in [Statuses](#statuses). A Subgraph error can include the affected block and a source location to help you correct the deployment. Each version has its own status: an upgrade failure does not change the status of a healthy serving version. * **Indexing unavailable** means indexing has stopped. Read the status explanation for the cause and the suggested next action. * **Status updated** tells you when the status was received, not which block has been indexed. If a measurement is unavailable or older than expected, check again before making a release decision. * **Get query endpoint**, in the **Actions** menu, connects through the Managed Subgraph's earliest created linked Endpoint Alias, which follows promotions. Without a linked Endpoint Alias, it connects to the current Deployment CID, which doesn't follow upgrades. Once you can query the deployment, include `_meta` to check the block and deployment serving your response. [Monitor sync health](./guides/monitor-sync-health) shows how to detect lag and indexing errors from your application. ## Statuses Each status label links here from its explanation. The Managed Subgraph and its Current version share one status; a Next version has its own while it is prepared for promotion. ### Setting up GraphOps is preparing the deployment for managed indexing, or hasn't received its first indexing status yet. No action is needed. ### Waiting to start The deployment is queued and indexing hasn't started. It starts without any action from you. ### Syncing The deployment is catching up to the network head. Queries succeed but can return older data, so check the sync lag before relying on recent blocks. ### Available The deployment is healthy and serving queries close to the network head. A small lag can remain; the page shows how many blocks it is behind. Routine maintenance, such as moving the deployment to different storage, keeps this status while the deployment keeps serving. ### Degraded Indexing health is reduced or hasn't been confirmed. Some data can be incomplete or delayed. Read the status explanation for details, and check again before making a release decision. ### Indexing unavailable Indexing has stopped, usually because of a Subgraph error. The explanation names the cause, the affected block when known, and the suggested next action. See [Managed indexing has stopped](./troubleshooting#managed-indexing-has-stopped). ### Pausing and Paused Managed indexing is being paused, or is paused. A paused deployment doesn't index new blocks. ### Removing and Removed The deployment is being removed, or has been removed, for example after a Next version was cancelled or the Managed Subgraph was archived. ### Preparing A Next version is being prepared for indexing. ### Checking readiness GraphOps is checking whether the Next version is ready to serve queries. Promotion waits until readiness is confirmed. ### Ready to promote The Next version is healthy and ready to become the Current version. Promote it, or let automatic promotion move it when its Target type follows The Graph Network. ### Cancelling Preparation of the Next version is being cancelled. The Current version keeps serving queries. ## Upgrade without changing your application URL Your **Current version** continues serving queries while you prepare a **Next version**. Your Endpoint Alias moves only when you promote: *Current serves queries through every stage of an upgrade. Your Endpoint Alias moves only when you promote a Next version that meets the readiness checks, and returning to the previous deployment takes another upgrade.* - Start upgrade: Enter the next Deployment CID on the same network. Review the schema comparison before you continue. (Endpoint Alias serves: Qm…a1f4; Current: Qm…a1f4 Available; Next: Qm…7c2e Preparing) - Index and test: Next indexes while Current keeps serving queries. Both versions accrue indexing charges. (Endpoint Alias serves: Qm…a1f4; Current: Qm…a1f4 Available; Next: Qm…7c2e Syncing) - Ready to promote: Next is healthy and meets the readiness checks. Test its query results before you promote it. (Endpoint Alias serves: Qm…a1f4; Current: Qm…a1f4 Available; Next: Qm…7c2e Ready to promote) - Promote version: Endpoint Aliases that target the Managed Subgraph move to the promoted version. The previous version accrues charges until its removal finishes. (Endpoint Alias serves: Qm…7c2e; Current: Qm…7c2e Available; Previous version: Qm…a1f4 Removing) 1. Choose **Start upgrade** from the page's actions and enter the next Deployment CID. GraphOps checks it as you enter it, and **Continue** becomes available once the check passes. It must use the same network; see [Deploy your own Subgraph](./guides/deploy-with-graph-cli) to get one. 2. Review the upgrade before you start it: the next version's details, what each linked Endpoint Alias does with it (Current version aliases move to it when you promote it; Next version aliases serve it while it indexes), and the schema comparison. Without a Next version alias, the review offers to create one. If the comparison reports breaking changes, update and test affected clients before proceeding. You can deliberately continue with a breaking change. If GraphOps can't start the upgrade with that version, the review sends you back to enter another; if the start fails for any other reason, start it again from the review. 3. Let the next version index, then test its query results. A compatible schema does not guarantee unchanged data or mapping behavior. 4. In **Versions**, choose **Promote version** when the next version is healthy and meets the service's indexing readiness checks. Review its indexing progress against the network head before promoting. An Endpoint Alias with a **Managed Subgraph** target and the **Current version** target behavior follows the version you promote, so your clients keep the same URL. To test the next version through a stable URL before promoting it, create a second Endpoint Alias with the **Next version** target behavior. It serves the Next version while it's being indexed and falls back to Current when you cancel the upgrade or Next can't serve. An Endpoint Alias with a **Deployment CID** target stays on that CID. Existing Endpoint Aliases only follow these upgrades after you explicitly target the Managed Subgraph. See [Manage Endpoint Aliases](./guides/manage-aliases#choose-a-target-behavior). You can cancel an upgrade before promotion. After promotion, returning to a previous deployment requires another upgrade; it is not an instant rollback. Keep the previous CID and test your recovery plan before a production release. [Develop and release a Subgraph](./guides/develop-and-release) uses these upgrades as a release workflow for your own Subgraph. ## Follow published upgrades automatically Choose a Graph Network Subgraph ID at creation, or [change the target type](#change-the-target-type) of an existing Managed Subgraph. Following authorizes indexing and promotion of future published versions, including breaking schema changes. Review the displayed schema comparison and prepare clients for changes published by the Subgraph owner. You don't stage these upgrades in GraphOps: * When the owner publishes a new version on The Graph Network, the latest version becomes Next while Current continues serving. * Next promotes when it is healthy, attached, and ready to serve queries. * If another release arrives first, the latest valid release replaces Next. Serving Current can therefore be older than the previous published network version. * Missing or deprecated IDs, lookup failures, invalid releases, and network changes hold the existing versions and Endpoint Alias mappings. Check the last resolution and tracking error in the Managed Subgraph's **Versions**. A paused Managed Subgraph that follows a Graph Network Subgraph ID keeps following it and resumes with the latest version. An Endpoint Alias with a **Graph Network Subgraph ID** target follows the same publications through indexers on The Graph Network, independently of this Managed Subgraph's indexing. Use a **Managed Subgraph** target when the Endpoint Alias should follow this Managed Subgraph's serving Current version. [Manage Endpoint Aliases](./guides/manage-aliases) shows when each target moves. ## Change the target type A Managed Subgraph's Target type decides how it moves to new versions. A **Deployment CID** target stays on the currently indexing deployment until you [upgrade it](#upgrade-without-changing-your-application-url). A **Graph Network Subgraph ID** target [follows published upgrades](#follow-published-upgrades-automatically). The name, version history, and linked Endpoint Aliases stay with the Managed Subgraph either way. 1. Choose **Change target type** from the Managed Subgraph's actions. 2. Choose what it should follow. For a Graph Network Subgraph ID, enter the ID and wait for it to resolve. 3. Review the target type, Current CID, and Next CID now and after saving, and what each linked Endpoint Alias serves. 4. Select **Save target type**. The change takes effect immediately, and the currently indexing deployment stays the Current version. To follow a Graph Network Subgraph ID, the Managed Subgraph must be active with no staged manual upgrade, and the ID must use the same blockchain network. The currently indexing deployment must also be one of the versions published to that ID; if it isn't, [publish it](./guides/deploy-with-graph-cli#publish-on-the-graph-network-optional) first. Promote or cancel a staged upgrade before you change the target type. To follow a different Subgraph ID, change the target type to a Deployment CID first. Changing to a Deployment CID stops following and retires a staged Next version: its indexing stops, and Next version Endpoint Aliases serve the Current version. Current version Endpoint Aliases are unchanged. **Start upgrade** becomes available again, so you stage each new version yourself. ## Plan indexing costs Managed indexing charges depend on how long each version receives indexing service, its entity count, and its stored data. Charges can accrue during setup, syncing, and degraded operation, not only when you send queries. Use the cost estimate to plan spending; it is not a final bill. Current rates as of Nov 12, 2026, 3:39 AM UTC. | Usage component | Applies to | Rate | | --- | --- | --- | | Queries | Default | 50 credits per query | | Queries | Billing mode: managed | 0 credits per query | | Response bytes | Default | 1,500,000 credits per GiB | | Response bytes | Billing mode: managed | 0 credits per GiB | | Managed allocated time | Default | 100,000 credits per hour | | Managed entity time | Default | 77,000 credits per 1 million entity-hours | | Managed storage time | Default | 3,850 credits per GiB-hour | The managed query rate applies when your organization's request resolves to its managed deployment, whether you use a Deployment CID, Endpoint Alias, or Graph Network Subgraph ID. During upgrades, both Current and Next can accrue indexing charges. The previous version can continue accruing charges until its indexing service is removed. Your organization limit counts the Managed Subgraph once, even while two versions are active. Separate Managed Subgraphs sharing a CID each count toward the limit, and their attached registrations are charged separately. If credits run out while your organization has a monthly plan, existing indexing can continue accruing charges until the grace period ends and disconnection is confirmed. Without a monthly plan, or while a plan payment needs attention, there is no grace period and indexing stops as soon as the balance reaches zero. [Automatic top-ups](../platform/billing-and-credits#automatic-top-ups) add credits before that happens. See [Managed Subgraphs during a pause](../platform/billing-and-credits#managed-subgraphs-during-a-pause) for stopping and recovery after adding credits. ## Check your organization's limit Each organization can have up to 10 Managed Subgraphs by default, whatever its plan size. Contact [GraphOps support](../platform/support-and-status) to raise the limit for your organization. If you add a Managed Subgraph without a monthly plan, the platform asks an owner or billing admin to choose one. If your organization has reached its limit, the platform points you to support. Paused Subgraphs and those being removed still count toward the limit. Room becomes available after removal finishes. ## Stop indexing a Subgraph Removal is irreversible and stops managed indexing. Check that your applications no longer depend on that service before proceeding. 1. If it follows a Graph Network Subgraph ID and has a Next version, use **Change target type** to change it to a Deployment CID, which retires the Next version. Otherwise, cancel a staged manual upgrade. 2. Delete the Endpoint Aliases that target this Managed Subgraph, or change them to Deployment CID targets. The delete dialog lists each one that still follows it, with links to change its target or delete it, and stays disabled until none remain. 3. Select **Delete** and confirm deletion of the Managed Subgraph. 4. Check that removal finishes before its place in your limit or its name becomes available again. The record remains under **Archived**, with its version history. Once removal finishes, you can reuse the name for a new Managed Subgraph; the archived record remains available. You can create another resource with the same CID while a peer is still active. ## Next steps * [Manage Endpoint Aliases](./guides/manage-aliases): Give your application a stable endpoint that follows promotions. * [Monitor sync health](./guides/monitor-sync-health): Check lag and indexing errors from your application with `_meta`. * [Troubleshooting](./troubleshooting): Resolve validation, status, or query errors. --- # Supported networks Source: https://docs.graphops.xyz/subgraphs/networks Check whether you can query a Subgraph or have GraphOps index it on your network. You can query any Subgraph that The Graph Network serves, on the query networks below. Managed indexing runs on a smaller set of networks. For a Managed Subgraph, validate the Deployment CID before starting indexing to check whether its network is supported. Managed Subgraphs and Endpoint Alias targets must each use exactly one network across all manifest data sources and templates. An Endpoint Alias does not change a deployment's network. ## Query networks GraphOps queries Subgraphs on every network The Graph Network serves. The manifest's network name maps to the listed CAIP-2 network ID. The selected Subgraph deployment determines the query network. | Network | Network ID (CAIP-2) | Manifest network | | --- | --- | --- | | Anubis Mainnet | eip155:6714 | anubis | | Arbitrum One | eip155:42161 | arbitrum-one | | Arbitrum Sepolia | eip155:421614 | arbitrum-sepolia | | Arc | eip155:5042 | arc | | Arc Network Testnet | eip155:5042002 | arc-testnet | | Avalanche C-Chain | eip155:43114 | avalanche | | Avalanche Fuji Testnet | eip155:43113 | fuji | | Base | eip155:8453 | base | | Base Sepolia Testnet | eip155:84532 | base-sepolia | | BNB Smart Chain Mainnet | eip155:56 | bsc | | BNB Smart Chain Testnet | eip155:97 | chapel | | Boba Network | eip155:288 | boba | | Boba Sepolia | eip155:28882 | boba-testnet | | Celo Mainnet | eip155:42220 | celo | | Celo Sepolia Testnet | eip155:11142220 | celo-sepolia | | Chiliz Chain Mainnet | eip155:88888 | chiliz | | Chiliz Spicy Testnet | eip155:88882 | chiliz-testnet | | Ethereum Hoodi | eip155:560048 | hoodi | | Ethereum Mainnet | eip155:1 | mainnet | | Ethereum Sepolia | eip155:11155111 | sepolia | | Etherlink Mainnet | eip155:42793 | etherlink-mainnet | | Etherlink Shadownet Testnet | eip155:127823 | etherlink-shadownet | | Fraxtal | eip155:252 | fraxtal | | Fuse Mainnet | eip155:122 | fuse | | Fuse Testnet | eip155:12001 | fuse-testnet | | Gnosis | eip155:100 | gnosis | | Gnosis Chiado Testnet | eip155:10200 | gnosis-chiado | | Hemi | eip155:43111 | hemi | | Hemi Sepolia | eip155:743111 | hemi-sepolia | | Injective | eip155:1776 | injective-evm | | Injective Testnet | eip155:1439 | injective-evm-testnet | | Japan Open Chain Mainnet | eip155:81 | joc | | Japan Open Chain Testnet | eip155:10081 | joc-testnet | | Kaia Kairos Testnet | eip155:1001 | kaia-testnet | | Kaia Mainnet | eip155:8217 | kaia | | Linea | eip155:59144 | linea | | Linea Sepolia | eip155:59141 | linea-sepolia | | Near Mainnet | near:mainnet | near-mainnet | | Near Testnet | near:testnet | near-testnet | | Neo X Mainnet | eip155:47763 | neox | | Neo X Testnet T4 | eip155:12227332 | neox-testnet | | OP Mainnet | eip155:10 | optimism | | OP Sepolia Testnet | eip155:11155420 | optimism-sepolia | | Peaq | eip155:3338 | peaq | | Polygon Amoy | eip155:80002 | polygon-amoy | | Polygon Mainnet | eip155:137 | matic | | Rootstock Mainnet | eip155:30 | rootstock | | Rootstock Testnet | eip155:31 | rootstock-testnet | | Scroll | eip155:534352 | scroll | | Scroll Sepolia Testnet | eip155:534351 | scroll-sepolia | | Sei Network | eip155:1329 | sei-mainnet | | Sei Testnet | eip155:1328 | sei-atlantic | | Soneium | eip155:1868 | soneium | | Soneium Testnet Minato | eip155:1946 | soneium-testnet | | Sonic Mainnet | eip155:146 | sonic | | Sonic Testnet | eip155:14601 | sonic-testnet | | Stable Mainnet | eip155:988 | stable | | Tempo Mainnet Presto | eip155:4217 | tempo | | Tempo Testnet Moderato | eip155:42431 | tempo-moderato | | Unichain | eip155:130 | unichain | | Unichain Sepolia Testnet | eip155:1301 | unichain-testnet | | Viction | eip155:88 | viction | | X Layer Mainnet | eip155:196 | xlayer-mainnet | | X Layer Sepolia Testnet | eip155:195 | xlayer-sepolia | | ZetaChain Mainnet | eip155:7000 | zetachain | | Zilliqa 2 | eip155:32769 | zilliqa | | Zilliqa 2 Testnet | eip155:33101 | zilliqa-testnet | | zkSync Mainnet | eip155:324 | zksync-era | | zkSync Sepolia Testnet | eip155:300 | zksync-era-sepolia | ## Managed indexing Use a deployment on one of the supported indexing networks below. The manifest's network name maps to the listed CAIP-2 network ID. | Network | Network ID (CAIP-2) | Manifest network | | --- | --- | --- | | Ethereum Mainnet | eip155:1 | mainnet | | Ethereum Sepolia | eip155:11155111 | sepolia | | Gnosis | eip155:100 | gnosis | | Base | eip155:8453 | base | | Arbitrum One | eip155:42161 | arbitrum-one | | Arbitrum Sepolia | eip155:421614 | arbitrum-sepolia | ## Query endpoints Subgraph endpoints are selected by Deployment CID, Graph Network Subgraph ID, or GraphOps Endpoint Alias. The network comes from the Subgraph you import or query. ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ https://subgraphs.graphops.xyz/v1/subgraphId/ https://subgraphs.graphops.xyz/v1/alias/ ``` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ Authorization: Bearer ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=subgraphs) [Supported networks](/subgraphs/networks) ## Testnets Testnet query availability follows The Graph Network: the query networks above include its testnets. Check the exact deployment before building test automation around it, because a Subgraph is queryable only while The Graph Network serves it. For managed indexing on a testnet, validate its CID or ask GraphOps support about network availability. Use the same endpoint shapes for testnet Subgraphs. The network comes from the Subgraph deployment or Graph Network Subgraph ID. ## Network notes * GraphOps supports any Subgraph on The Graph Network available for queries by Deployment CID or Graph Network Subgraph ID, on any of the query networks above. * An Endpoint Alias targets a Deployment CID, follows a Graph Network Subgraph ID, or follows a Managed Subgraph. The target determines the deployment queried. * Schema compatibility still depends on the specific Subgraph version. * GraphOps Subgraphs are reorg-aware and expose indexed block metadata through `_meta`. ## Next steps * [Managed Subgraphs](/subgraphs/managed-subgraphs): Start indexing a deployment on a supported network. * [Identifiers and Endpoint Aliases](/subgraphs/concepts/identifiers-and-aliases): Choose the identifier that selects your deployment. * [Sync and reorgs](/subgraphs/concepts/sync-and-reorgs): Plan for indexing lag and reorg corrections on any network. --- # Schema reference Source: https://docs.graphops.xyz/subgraphs/schema-reference Find the entities, fields, and filters you can query on a specific Subgraph. Every Subgraph has its own GraphQL schema, so GraphOps does not publish one universal entity reference. This page shows how to discover a schema and documents the `_meta` fields most Subgraphs share. ## Discover the schema Use GraphQL introspection, the schema published with the Subgraph project, or the schema map in [Discover Subgraphs](./guides/discover-public-subgraphs) to discover: * Entity names. * Field names and scalar types. * Relationship fields. * Generated filters. * Generated ordering fields. * Whether full-text search fields are present. ## Introspection query ```graphql query IntrospectionTypes { __schema { queryType { fields { name } } types { name kind } } } ``` Use the same endpoint and authentication method as application queries. ## Metadata reference Most Subgraphs expose `_meta`: | Field | Meaning | | ------------------------- | ------------------------------------------------------ | | `_meta.deployment` | Deployment serving the query. | | `_meta.block.number` | Latest indexed block number available to the endpoint. | | `_meta.block.hash` | Hash of the indexed block. | | `_meta.hasIndexingErrors` | Whether indexing errors are present. | ```graphql query Metadata { _meta { deployment block { number hash } hasIndexingErrors } } ``` ## Generated client types GraphOps does not provide service-level SDKs for Subgraphs. If your application needs generated types, generate them from the specific Subgraph schema using your GraphQL client toolchain. ## Next steps * [Query a Subgraph](./guides/query-a-subgraph): Turn the schema into a parameterized query. * [Paginate results](./guides/paginate-results): Order by a stable field and read large collections. * [Monitor sync health](./guides/monitor-sync-health): Use `_meta` for freshness and error alerts. --- # Troubleshooting Source: https://docs.graphops.xyz/subgraphs/troubleshooting Find the error or symptom you see, then work through its checks in order. If the issue remains after the checks, contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) with the details listed under [Contact support](#contact-support). ## `401 Unauthorized` The request is missing a valid API key, the `Authorization` header is malformed, or the route-token URL does not include a valid key. 1. Confirm the key was created for the organization that owns Subgraphs access. 2. For header auth, confirm the header is exactly `Authorization: Bearer `. 3. For route-token auth, confirm the endpoint ends with `/`. 4. Confirm the key has not been revoked or rotated. 5. Remove extra quotes, whitespace, or the literal string `Bearer` from secret-manager values. 6. Use [Authentication](./authentication) to compare header and route-token formats. ## `403 Forbidden` The key is valid, but service access for the organization is paused, usually because its credit balance reached zero. 1. Check your credit balance and whether service access is paused in **Billing**. 2. Add credits to bring the balance above zero. Access is restored automatically. To avoid a repeat, turn on [automatic top-ups](/platform/billing-and-credits#automatic-top-ups). 3. Confirm the API key belongs to the same organization that owns the Endpoint Alias or Managed Subgraph. 4. If GraphOps support paused the organization, adding credits does not restore access. Contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open). ## `404 Not Found` The Deployment CID, Graph Network Subgraph ID, or Endpoint Alias does not resolve for the organization. 1. Confirm the endpoint mode: `deploymentCID`, `subgraphId`, or `alias`. 2. Confirm the identifier value in the platform UI. 3. For Endpoint Aliases, confirm the Endpoint Alias name is correct. 4. For Deployment CIDs, confirm the CID identifies a deployed Subgraph. 5. For Graph Network Subgraph IDs, confirm the Subgraph exists on The Graph Network. ## Subgraph unavailable: no indexer is serving the deployment The deployment has no active indexer allocations on The Graph Network. Its publication or presence in the discovery catalog does not guarantee that an indexer can serve queries. The explorer reports this as **Subgraph unavailable**; a direct query can return `subgraph not found: no allocations` inside a GraphQL `errors` array even with HTTP status `200`. 1. Confirm the Deployment CID you want to query, including the current version if you use a Graph Network Subgraph ID or an Endpoint Alias that follows one. 2. An Owner or Developer can [import it as a Managed Subgraph](./guides/import-existing-subgraph) to start GraphOps managed indexing if its network is supported. If you have another role, ask an Owner or Developer to do this, or choose another deployment with available indexing. 3. Once indexing is ready, choose **Retry** in the explorer. Repeated requests alone do not create indexer coverage. ## Subgraph unavailable: indexers report an indexing error Every indexer serving the deployment has stopped on an indexing error, so The Graph Network does not answer queries for it. The explorer reports this as **Subgraph unavailable** and can still show the deployment's metadata. A direct query can return `bad indexers: {…: BadResponse(no attestation: indexing_error)}` inside a GraphQL `errors` array. 1. Check the Subgraph's current version. A publisher usually fixes an indexing error by publishing a new deployment, and a Graph Network Subgraph ID or an Endpoint Alias that follows one moves to it. 2. Otherwise choose another deployment, or ask the publisher to fix the Subgraph and redeploy it. Retrying the same deployment does not clear the error. ## Queries fail or return less data after a new version is published Your endpoint moved to a new version that is not fully indexed yet. A `subgraphId` endpoint, or an Endpoint Alias with a Graph Network Subgraph ID target and the Latest published version target behavior, moves as soon as the owner publishes a version. Until the new version catches up, queries can report that no indexer is serving the deployment, return fewer entities or an older `_meta.block.number`, or fail validation against a changed schema. 1. Query `_meta.deployment` and compare it with the Deployment CID you tested against. 2. To keep serving the previous version now, query its `deploymentCID` endpoint, or change your Endpoint Alias to a **Deployment CID** target for that CID. This works while an indexer still serves the previous version. 3. To avoid the gap in future, point your application at an Endpoint Alias with the Latest available version target behavior, which waits for indexers to serve each new version, or at one that targets a Managed Subgraph that follows the Subgraph. See [Choose your Subgraph setup](./choose-a-setup). ## Endpoint Alias hasn't moved to the latest version An Endpoint Alias with a Graph Network Subgraph ID target and the Latest available version target behavior keeps serving the previous version until an indexer on The Graph Network reports the latest one healthy and close to the chain head. 1. Open [Endpoint Aliases](https://platform.graphops.xyz/organizations/current/services/subgraphs/aliases) and find the Endpoint Alias. The **Target** column shows the **Latest** Deployment CID and why it isn't serving yet. 2. Wait for indexers to catch up on the new version. The Endpoint Alias moves on its own once they do. 3. To serve the latest version now, change the target behavior to Latest published version, or change the target to its Deployment CID. Queries can fail or return incomplete data until indexers catch up. 4. If no indexer serves the new version for a long time, an Owner or Developer can [import it as a Managed Subgraph](./guides/import-existing-subgraph) to index it with GraphOps. ## GraphQL validation errors The query references a field, filter, ordering value, or entity type that does not exist in the Subgraph schema. The response has an `errors` array, often with HTTP status `200`. 1. Run schema introspection or inspect the schema published with the Subgraph. 2. Confirm entity names and field casing. 3. Confirm generated filters such as `id_gt` or `createdAtTimestamp_gte` exist for that entity. 4. Confirm an Endpoint Alias update did not move the endpoint to a schema-incompatible deployment. 5. Use [Schema reference](./schema-reference) for schema discovery guidance. ## Empty or stale results The Subgraph may not have indexed the expected block yet, the query may be too restrictive, or the application may be querying a different deployment than expected. 1. Query `_meta.block.number` and `_meta.deployment`, or read the `X-Deployment-CID` response header. 2. Confirm the endpoint mode and identifier. 3. Confirm filters and ordering fields match the schema. 4. If using an Endpoint Alias, compare `X-Deployment-CID` or `_meta.deployment` with the serving Deployment CID shown for it in the platform UI. A Deployment CID target stays on that deployment. A Managed Subgraph target serves its Current version, or its Next version, which can be partially indexed, with the Next version behavior. A Graph Network Subgraph ID target with Latest published version can move before indexers serve the new version. 5. Compare `_meta.block.number` with your expected chain head only when you have a reliable head source for the same network. ## `_meta.hasIndexingErrors` is `true` The deployment serving the query reports indexing errors, so some entities may be missing or stale. 1. Log `_meta.deployment`, `_meta.block.number`, and `_meta.block.hash`. 2. Confirm whether the errors started after a Subgraph deployment or Endpoint Alias update. 3. Test a previous Deployment CID if you keep release records. 4. Contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) with the endpoint and `_meta` values. ## `429 Too Many Requests` The organization exceeded a query-rate or concurrency limit. 1. Reduce parallel requests. 2. Add exponential backoff with jitter. 3. Page large scans with stable cursors. 4. Avoid running schema introspection on a hot path. 5. Request a limit increase through [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open). ## Managed Subgraph validation or status is unavailable The deployment may use a network that managed indexing does not support, or the latest status could not be received. 1. Validate the immutable Deployment CID in the platform UI. 2. Confirm that the deployment uses one [managed indexing network](./networks#managed-indexing) across all data sources and templates. 3. On the detail page, hover over, focus, or tap the status label for its explanation. Check indexing progress and **Status updated** time. The timestamp shows when the status was last received; it does not mean indexing has caught up with the network. 4. If a status or measurement shows **Unavailable**, refresh the detail page before starting, promoting, or removing the resource. Contact support if the information remains unavailable or you cannot complete the action. ## A Managed Subgraph can't follow a Graph Network Subgraph ID **Change target type** keeps the Graph Network Subgraph ID choice disabled, or the ID you entered shows an error. 1. If a Next version is staged, promote it or cancel the upgrade first. 2. Check that the ID's current version uses the same network as the Managed Subgraph. 3. Check that the currently indexing deployment is one of the versions published to the ID. If it isn't, [publish it](./guides/deploy-with-graph-cli#publish-on-the-graph-network-optional) to the ID first. 4. To follow a different ID than the one it follows now, change the target type to a Deployment CID first. See [Change the target type](./managed-subgraphs#change-the-target-type) for every requirement. ## Managed indexing has stopped Open the explanation on the affected version's status label. If it identifies an error in your Subgraph, use the affected block and source location, when provided, to inspect the mapping, schema, entity data, ABI or manifest. Deploy a corrected version after resolving the cause. If the explanation asks you to contact support, include the Deployment CID, the affected block and the **Status updated** time. Check Current and Next separately: a failed upgrade can leave the serving version healthy. ## Contact support When contacting [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open), include: * Organization name or ID. * Endpoint mode: Deployment CID, Graph Network Subgraph ID, or Endpoint Alias. * Identifier value, with API keys redacted. * Approximate timestamp. * HTTP status or GraphQL error message. * The `X-Deployment-CID` response header, and `_meta.deployment`, `_meta.block.number`, and `_meta.block.hash` when available. * Whether the request used header auth or route-token auth. ## Next steps * [Monitor sync health](./guides/monitor-sync-health): Catch lag and indexing errors before users do. * [Authentication](./authentication): Compare header and route-token formats. * [Usage and limits](./usage-and-limits): Check rate limits, retries, and how queries are charged. --- # Usage and limits Source: https://docs.graphops.xyz/subgraphs/usage-and-limits Budget your Subgraphs spending and size your workloads to your organization's limits. Queries and Managed Subgraph indexing have separate rates. Each query uses a query rate, and a Managed Subgraph also accrues indexing charges while GraphOps keeps it indexed, not only when you send queries. ## Billing model | Mode | Billing behavior | Use it when | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | Standard | Each GraphQL request uses the standard query rate shown below. | Your deployment is not registered as a Managed Subgraph. | | Managed Subgraph | Indexing charges accrue hourly based on attached time, entity count, and storage. Queries use the Managed Subgraph rate shown below. | You want GraphOps to keep your deployment indexed and manage version upgrades. | ## Current credit rates The rates below show the current query and indexing charges. Managed Subgraph estimates in the platform use current indexing measurements and these rates; an estimate is not a bill. Current rates as of Nov 12, 2026, 3:39 AM UTC. | Usage component | Applies to | Rate | | --- | --- | --- | | Queries | Default | 50 credits per query | | Queries | Billing mode: managed | 0 credits per query | | Response bytes | Default | 1,500,000 credits per GiB | | Response bytes | Billing mode: managed | 0 credits per GiB | | Managed allocated time | Default | 100,000 credits per hour | | Managed entity time | Default | 77,000 credits per 1 million entity-hours | | Managed storage time | Default | 3,850 credits per GiB-hour | During an upgrade, Current and Next can both accrue indexing usage while they are prepared. A previous version may continue accruing briefly while its service is removed after promotion. ## Organization limits These limits apply to your whole organization, not to each API key, and they do not change with your plan size. [Contact support](/platform/support-and-status) for the values that apply to your workload or to request an increase. The primary operational limit is query rate. Each organization can also have up to 10 Managed Subgraphs by default; see [Managed Subgraphs](./managed-subgraphs#check-your-organizations-limit). | Limit | Applies to | Guidance | | ----------------------- | ----------------------------------------- | --------------------------------------------------------- | | Query rate | GraphQL requests for the organization | Bound worker concurrency and back off after `429`. | | Query timeout | Long-running or expensive GraphQL queries | Reduce selection sets, filters, or page size. | | Response size | Large entity pages and nested selections | Page results and request only required fields. | | Query complexity | Deeply nested or expensive queries | Prefer multiple bounded queries over one unbounded query. | | Introspection frequency | Schema discovery and code generation | Run introspection in development or CI, not per request. | ## Retry guidance Retry transient `429`, `502`, `503`, and `504` responses with exponential backoff and jitter. For large entity scans, persist pagination state and retry from the last completed cursor. Do not retry a query indefinitely if it returns GraphQL validation errors. Validation errors usually mean the query does not match the Subgraph schema. ## Production checklist * Store API keys outside source code. See [Authentication](./authentication). * Use Endpoint Aliases targeting Managed Subgraphs for production endpoints that should follow their upgrades. * Include `_meta` in health checks and important reads. * Bound page sizes and use cursor-like pagination for long scans. * Use the Managed Subgraph status timestamp when diagnosing unexpected usage. * Contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) when limits need to be raised. ## Next steps * [Platform: Usage and limits](/platform/usage-and-limits): Track recorded consumption, billing-window timing, and your limit values. * [Managed Subgraphs](./managed-subgraphs): Plan indexing costs across upgrades and removal. * [Paginate results](./guides/paginate-results): Keep large scans inside rate and response-size limits. --- # API integration Source: https://docs.graphops.xyz/rpc/api Use this reference to configure endpoints, shape requests, match batch responses, and handle errors. GraphOps RPC exposes Ethereum-compatible JSON-RPC over HTTPS and WebSocket, plus Bitcoin JSON-RPC over HTTPS. For individual methods, see [Supported methods](./methods). ## Endpoint patterns ```text https://rpc.graphops.xyz/v1/evm/ wss://rpc.graphops.xyz/v1/evm/ ``` _An RPC endpoint names the network in its path: the namespace, then the EVM chain ID. This example is Base (8453). Send the API key in the Authorization header or append it to the path._ ```text https://rpc.graphops.xyz/v1/evm/8453 Authorization: Bearer ``` - RPC host: `https://rpc.graphops.xyz` - Namespace: `evm` - EVM chain ID: `8453` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://rpc.graphops.xyz/v1/evm/1 Authorization: Bearer ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=rpc&networkId=eip155%3A1) [Supported networks](/rpc/networks) Use the concrete URL for your target chain in application configuration or client setup. See [Supported networks](./networks) for current query endpoints and archive coverage. ### Bitcoin endpoints Bitcoin RPC uses HTTPS and a network name instead of an EVM chain ID. The endpoint route is: ```text https://rpc.graphops.xyz/v1/bitcoin/ ``` For example, this request reads Bitcoin mainnet chain information. EVM client libraries do not support Bitcoin JSON-RPC, so the programs use an HTTP client: Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.api.sh-3.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/bitcoin/mainnet" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "getblockchaininfo", "params": [] }' ``` Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/rpc.api.ts-14.tar.gz) ```typescript export {}; const response = await fetch("https://rpc.graphops.xyz/v1/bitcoin/mainnet", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getblockchaininfo", params: [], }), }); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); console.log(JSON.stringify(await response.json())); ``` Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0. [Example project](/examples/rpc.api.go-15.tar.gz) ```go package main import ( "bytes" "fmt" "io" "log" "net/http" "os" "time" ) func main() { payload := []byte(`{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}`) req, err := http.NewRequest(http.MethodPost, "https://rpc.graphops.xyz/v1/bitcoin/mainnet", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } fmt.Println(string(body)) } ``` Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/rpc.api.rust-16.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { let response = reqwest::Client::new() .post("https://rpc.graphops.xyz/v1/bitcoin/mainnet") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "getblockchaininfo", "params": [], })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; println!("{result}"); Ok(()) } ``` Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/rpc.api.python-17.tar.gz) ```python import json import os import requests response = requests.post( "https://rpc.graphops.xyz/v1/bitcoin/mainnet", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "jsonrpc": "2.0", "id": 1, "method": "getblockchaininfo", "params": [], }, timeout=30, ) response.raise_for_status() print(json.dumps(response.json())) ``` Clients that cannot attach headers can use the route-token form instead: ```text https://rpc.graphops.xyz/v1/bitcoin/mainnet/ ``` ## Authentication Authenticate with an organization-scoped API key. RPC supports header authentication and route-token authentication. See [Authentication](./authentication) for key creation, rotation, WebSocket connections, and secret handling. ## Request envelope Every EVM and Bitcoin JSON-RPC call uses the standard request envelope. Method names and parameter formats are network-specific; the examples in this section use EVM methods. | Field | Type | Required | Notes | | --------- | ----------------------- | ----------------------------------- | ------------------------------------------------------------------------- | | `jsonrpc` | string | Yes | Must be `"2.0"`. | | `id` | string, number, or null | Required for request/response calls | Use a stable value so clients can match responses, especially in batches. | | `method` | string | Yes | Standard Ethereum-compatible method name, such as `eth_blockNumber`. | | `params` | array or object | Method-specific | Ethereum JSON-RPC methods usually use positional arrays. | ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] } ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x191161d" } ``` JSON-RPC version Matches the id you sent Latest block in decimal: 26,285,597 ## HTTPS requests HTTPS requests use `POST` with `Content-Type: application/json`. The examples below use Ethereum mainnet. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.api.sh-6.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x191161d" } ``` JSON-RPC version Matches the id you sent Latest block in decimal: 26,285,597 Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.api.ts-7.tar.gz) ```typescript import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const blockNumber = await client.getBlockNumber(); console.log(blockNumber.toString()); ``` Output ```json 26285599 ``` Latest block in decimal: 26,285,599 Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.api.go-12.tar.gz) ```go package main import ( "context" "fmt" "log" "os" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() blockNumber, err := client.BlockNumber(ctx) if err != nil { log.Fatal(err) } fmt.Println(blockNumber) } ``` Output ```json 26285612 ``` Latest block in decimal: 26,285,612 Prerequisites 1. Install Rust. 2. Install alloy, reqwest, and tokio. `cargo add alloy reqwest tokio -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0. [Example project](/examples/rpc.api.rust-13.tar.gz) ```rust use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::client::RpcClient; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use std::{env, error::Error}; #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); let block_number = provider.get_block_number().await?; println!("{block_number}"); Ok(()) } ``` Output ```json 26285610 ``` Latest block in decimal: 26,285,610 Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.api.python-9.tar.gz) ```python import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) print(w3.eth.block_number) ``` Output ```json 26285600 ``` Latest block in decimal: 26,285,600 ## Batch requests Batch requests are supported. Use them when several independent calls can share one HTTP round trip. Do not use a batch when later calls depend on earlier results. ```json [ { "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }, { "jsonrpc": "2.0", "id": 2, "method": "eth_chainId", "params": [] }, { "jsonrpc": "2.0", "id": 3, "method": "eth_getBalance", "params": ["0xnot-an-address", "latest"] } ] ``` Output ```json [ { "jsonrpc": "2.0", "id": 1, "result": "0x191162c" }, { "jsonrpc": "2.0", "id": 2, "result": "0x1" }, { "jsonrpc": "2.0", "id": 3, "error": { "code": -32602, "message": "Invalid params", "data": "invalid string length at line 1 column 18" } } ] ``` eth\_blockNumber in decimal: 26,285,612 Ethereum mainnet chain ID: 1 Only this call failed: invalid parameters In the output, the third call fails because its address is invalid; the other two succeed. Match responses by `id`; do not rely on response ordering. Retry only the failed items that are retryable, as a smaller batch or as single requests. ## WebSocket connections Use WebSocket endpoints for subscriptions and long-lived event streams. Route-token authentication works with WebSocket clients that cannot attach custom headers during the handshake. Prerequisites 1. Install Node.js. 2. Install tsx and ws. `npm install tsx ws` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, ws 8.22.0. [Example project](/examples/rpc.api.ts-11.tar.gz) ```typescript import WebSocket from "ws"; // Route-token URL: the API key is the final path segment. const socket = new WebSocket( `wss://rpc.graphops.xyz/v1/evm/1/${process.env.GRAPHOPS_API_KEY}`, ); socket.once("error", (error) => { throw error; }); socket.once("open", () => { console.log(JSON.stringify({ connected: true })); socket.close(); }); ``` See [Authentication](./authentication) for header authentication, route-token authentication, and security guidance. ## WebSocket subscriptions GraphOps supports standard Ethereum subscription flows over WebSocket: | Subscription | Use it for | | ------------------------ | ------------------------------------------------------------------ | | `newHeads` | Receive new block headers. | | `logs` | Receive logs matching an address and topic filter. | | `newPendingTransactions` | Receive pending transaction hashes when available for the network. | | `syncing` | Receive node sync status updates. | Persist the last processed block or log cursor in your application. If a WebSocket disconnects, use [WebSocket subscriptions](./guides/websocket-subscriptions) for recovery patterns. ## Error responses JSON-RPC errors use the standard `error` object in place of `result`: ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params": [ "invalid-address", "latest" ] } ``` Output ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "odd number of digits" } } ``` The call that failed Present instead of result Invalid params; see the table below Points at the first parameter Common error classes: | Code or status | Meaning | Typical response | | -------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------- | | `400` | Malformed HTTP request or invalid JSON body. | Fix request encoding. | | `401` | Missing or invalid API key. | Check header or route-token authentication. | | `403` | Service access for the organization is paused, usually because credits ran out. | Check your credit balance and service access in Billing. | | `429` | Organization exceeded rate or concurrency limits. | Back off and reduce concurrency. | | `-32700` | JSON parse error. | Send valid JSON. | | `-32600` | Invalid JSON-RPC request envelope. | Include `jsonrpc`, `method`, and valid `id` when needed. | | `-32601` | Method not found. | Check method spelling, transport, and network support. | | `-32602` | Invalid params. | Check parameter order, hex encoding, and block tag. | | `-32603` | Internal JSON-RPC error. | Retry if transient; contact support if persistent. | | `-32000` to `-32099` | Server or EVM execution error. | Inspect the message and method-specific context. | ## Next steps * [Supported methods](./methods): Check parameters, results, and transports for each EVM method. * [Retries and reorgs](./guides/retries-and-reorgs): Decide which of these errors to retry and how. * [Usage and limits](./usage-and-limits): Plan for rate, concurrency, batch, and response-size limits. --- # Authentication Source: https://docs.graphops.xyz/rpc/authentication Choose how your clients send their API key, and keep that key out of places it can leak. GraphOps RPC accepts an organization-scoped API key either in the `Authorization` header or in the endpoint path. Use one authentication method per request. ## Create and rotate API keys Create and rotate RPC API keys in the GraphOps Platform. The platform only shows the plaintext key at creation or rotation time, so store it in your secret manager immediately. Each key belongs to one organization and product. Use separate keys for production, staging, and local development so you can replace one environment's credentials without affecting the others. [Create or rotate an RPC key](https://platform.graphops.xyz/organizations/current/api-keys) ## Authentication methods | Method | Endpoint or header | Use it for | | -------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- | | Header authentication | `Authorization: Bearer ` | HTTPS requests and server-side clients that can attach headers. | | Route-token authentication | `https://rpc.graphops.xyz/v1/evm//` | HTTPS clients that cannot attach custom headers. | | Route-token authentication | `wss://rpc.graphops.xyz/v1/evm//` | WebSocket clients, including browser-native WebSocket clients. | | Route-token authentication | `https://rpc.graphops.xyz/v1/bitcoin//` | Bitcoin HTTPS clients that cannot attach custom headers. | Prefer header authentication when your client can set headers. Use route-token authentication when a client cannot attach `Authorization` during the connection, especially for WebSocket handshakes. *With header authentication the key travels only in the Authorization header. With route-token authentication it is part of the URL, and every system that records the URL records the key, so redact the path after the chain ID or network name.* - Header authentication: `https://rpc.graphops.xyz/v1/evm/1` and `Authorization: Bearer `. The URL holds no secret. - Route-token authentication: `wss://rpc.graphops.xyz/v1/evm/1/`. The URL is the secret. - Full URLs are often copied to: proxy logs, browser history, metrics labels, crash reports. ## Header authentication Send the API key as a bearer token: ```http Authorization: Bearer ``` Example HTTPS endpoint: ```text https://rpc.graphops.xyz/v1/evm/1 ``` Use header authentication for server-side HTTPS clients, backend workers, and libraries that support custom request headers. ## Route-token authentication ### EVM route-token endpoints Put the API key after the chain ID: ```text https://rpc.graphops.xyz/v1/evm// wss://rpc.graphops.xyz/v1/evm// ``` For Ethereum mainnet, the route-token endpoints are: ```text https://rpc.graphops.xyz/v1/evm/1/ wss://rpc.graphops.xyz/v1/evm/1/ ``` ### Bitcoin route-token endpoints Put the API key after a network name from [Supported networks](./networks): ```text https://rpc.graphops.xyz/v1/bitcoin// https://rpc.graphops.xyz/v1/bitcoin/mainnet/ ``` Use route-token authentication for WebSocket clients and HTTPS clients that cannot attach custom headers. Do not send the same API key in both the URL and the `Authorization` header. > **Treat route-token URLs as secrets** > > Route-token authentication puts the API key in the URL. URLs can appear in application logs, > reverse-proxy logs, browser history, crash reports, metrics labels, and support screenshots. Redact > the path after the chain ID for EVM endpoints or after the network name for Bitcoin endpoints, and > rotate the key if a route-token URL is exposed. ## WebSocket authentication Route-token authentication is the recommended WebSocket pattern because many WebSocket clients cannot send custom `Authorization` headers during the handshake. Prerequisites 1. Install Node.js. 2. Install tsx and ws. `npm install tsx ws` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, ws 8.22.0. [Example project](/examples/rpc.authentication.ts-6.tar.gz) ```typescript import WebSocket from "ws"; // Route-token URL: the API key is the final path segment. const socket = new WebSocket( `wss://rpc.graphops.xyz/v1/evm/1/${process.env.GRAPHOPS_API_KEY}`, ); socket.once("error", (error) => { throw error; }); socket.once("open", () => { console.log(JSON.stringify({ connected: true })); socket.close(); }); ``` If your server-side WebSocket client supports custom headers, header authentication is also valid: Prerequisites 1. Install Node.js. 2. Install tsx and ws. `npm install tsx ws` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, ws 8.22.0. [Example project](/examples/rpc.authentication.ts-7.tar.gz) ```typescript import WebSocket from "ws"; const socket = new WebSocket("wss://rpc.graphops.xyz/v1/evm/1", { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }); socket.once("error", (error) => { throw error; }); socket.once("open", () => { console.log(JSON.stringify({ connected: true })); socket.close(); }); ``` ## Security guidance * Store API keys in a server-side secret manager when possible. * Keep organization API keys out of public frontend bundles; send requests through your backend. * Use separate keys per environment. A route-token URL exposes the key to anyone who can read it. * Redact API keys from URLs before logging request paths, WebSocket URLs, errors, or metrics. * Rotate keys from the platform when a key or route-token URL is exposed. ## Next steps * [Troubleshooting](./troubleshooting#401-unauthorized): Work through the checks for a `401 Unauthorized` response. * [WebSocket subscriptions](./guides/websocket-subscriptions): Subscribe to live logs with a route-token endpoint. * [Supported networks](./networks): Copy the endpoint and chain ID for your network. --- # Make your first RPC request Source: https://docs.graphops.xyz/rpc/getting-started Confirm that your application can reach GraphOps RPC by reading the latest Ethereum block number. Use curl or your preferred EVM client library. **Expected result:** the current block number: a hex quantity from curl, a decimal number from the client libraries. ## Prerequisites * A GraphOps Platform account. * An organization with access to RPC. * An Owner or Developer role to create an RPC API key, or a key supplied by your team. * Ethereum mainnet endpoint access. The endpoint path ends in the EVM chain ID. These examples use Ethereum mainnet (`1`) and read your API key from the `GRAPHOPS_API_KEY` environment variable. For other endpoints, see [Supported networks](./networks). ### Create an API key \[#create-an-api-key] [Create an RPC API key](https://platform.graphops.xyz/organizations/current/api-keys) Choose your organization and create the key for RPC. Store the plaintext key in your secret manager. The platform only shows the plaintext key at creation or rotation time. ### Choose an endpoint \[#choose-an-endpoint] Choose the network endpoint. This guide uses Ethereum mainnet: ```text https://rpc.graphops.xyz/v1/evm/1 ``` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://rpc.graphops.xyz/v1/evm/1 Authorization: Bearer ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=rpc&networkId=eip155%3A1) [Supported networks](/rpc/networks) Mainnet and testnet endpoints use the same authentication model. See [Authentication](./authentication) for header and route-token options. ### Read the latest block \[#read-the-latest-block] Run an `eth_blockNumber` request against the Ethereum mainnet endpoint. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.api.sh-6.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x191161d" } ``` JSON-RPC version Matches the id you sent Latest block in decimal: 26,285,597 Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.api.ts-7.tar.gz) ```typescript import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const blockNumber = await client.getBlockNumber(); console.log(blockNumber.toString()); ``` Output ```json 26285599 ``` Latest block in decimal: 26,285,599 Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.api.go-12.tar.gz) ```go package main import ( "context" "fmt" "log" "os" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() blockNumber, err := client.BlockNumber(ctx) if err != nil { log.Fatal(err) } fmt.Println(blockNumber) } ``` Output ```json 26285612 ``` Latest block in decimal: 26,285,612 Prerequisites 1. Install Rust. 2. Install alloy, reqwest, and tokio. `cargo add alloy reqwest tokio -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0. [Example project](/examples/rpc.api.rust-13.tar.gz) ```rust use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::client::RpcClient; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use std::{env, error::Error}; #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); let block_number = provider.get_block_number().await?; println!("{block_number}"); Ok(()) } ``` Output ```json 26285610 ``` Latest block in decimal: 26,285,610 Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.api.python-9.tar.gz) ```python import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) print(w3.eth.block_number) ``` Output ```json 26285600 ``` Latest block in decimal: 26,285,600 ## Next steps * [Fetch logs](./guides/fetch-logs): Read the USDC `Transfer` logs from one block and keep larger scans bounded. * [Read contract state](./guides/read-contract-state): Call a contract with `eth_call` at a block you choose. * [Authentication](./authentication): Choose between header and route-token authentication before you deploy. * [Usage and limits](./usage-and-limits): Check credit rates and shared limits before sending production traffic. --- # GraphOps RPC Source: https://docs.graphops.xyz/rpc Read balances and contract state, submit signed transactions, and react to new blocks without running your own nodes. GraphOps RPC works with familiar EVM libraries over HTTPS and WebSocket, and supports Bitcoin RPC over HTTPS. ## RPC at a glance _Your app sends JSON-RPC requests over HTTPS or WebSocket. GraphOps nodes follow the chain and answer each one._ - Your app: JSON-RPC - RPC: Nodes - Chain: New blocks - [Run the RPC quickstart](/rpc/getting-started) - [Open the RPC reference](/rpc/methods) ## Choose RPC when Use RPC when your application needs direct node calls or already uses an EVM library such as `viem`, go-ethereum's `ethclient`, `alloy`, or `web3.py`. You control how calls are combined and how results are stored. For an indexed GraphQL data model, choose [Subgraphs](/subgraphs). For an ordered stream feeding your own database, choose [Substreams](/substreams). ## What you can build Each pattern uses standard EVM JSON-RPC methods and works with any EVM client library. Your application decides how requests combine and where results are stored. - [Consistent reads at one block](/rpc/guides/read-contract-state): Price a pool or value a position from several eth_call reads pinned to the same block. - [Event history in bounded ranges](/rpc/guides/backfill-historical-events): Build a transfer history with eth_getLogs, one resumable block range at a time. - [Transaction submission and receipts](/rpc/guides/send-raw-transaction): Broadcast a transaction you signed, store its hash, and poll for the receipt. - [Live events over WebSocket](/rpc/guides/websocket-subscriptions): React to new heads and matching logs, and backfill any gap after a reconnect. Bitcoin applications can call Bitcoin Core-compatible methods over HTTPS; see [Bitcoin endpoints](/rpc/api#bitcoin-endpoints). ## What you run and what GraphOps runs *GraphOps runs the nodes behind authenticated endpoints and meters usage. Your application signs transactions, chooses block references, retries, and stores anything it builds from responses.* Your application: - Signing keys and nonces - Block references for related reads - Retries, batching, and backoff - Storage built from responses Endpoint: `rpc.graphops.xyz/v1/evm/` (API key in a header or the path). GraphOps operates: - EVM and Bitcoin nodes - HTTPS for EVM and Bitcoin, WebSocket for EVM - API keys, metering, and credits ## Supported networks Use the live network catalog to choose an EVM or Bitcoin endpoint and check archive coverage. [Choose your network and copy its endpoint](/rpc/networks). ## Integration paths EVM RPC supports both HTTPS and WebSocket transports. Bitcoin RPC currently supports HTTPS. | Transport | Use it for | | --------- | ------------------------------------------------------------------------------------ | | HTTPS | Single requests, batch requests, reads, transaction submission, and polling filters. | | WebSocket | Subscriptions such as new heads, logs, pending transactions, and syncing updates. | Authenticate requests with an organization-scoped API key. Use header authentication for clients that can attach headers, or route-token authentication for WebSocket clients and HTTPS clients that cannot attach custom headers. See [Authentication](/rpc/authentication). ## Production behavior Separate `latest` reads can observe different chain states. If a workflow compares several values, select a block first and reuse its number or hash for every call. Choose a finalized block when you need stable results more than the newest data. Calls and subscription events consume credits at their current rates. Plan for shared request and concurrency limits across your organization. ## Next steps * [Make your first RPC request](/rpc/getting-started): Create an API key and read the latest Ethereum block. * [Consistency and finality](/rpc/concepts/consistency-and-finality): Choose a block reference before combining several reads. * [Usage and limits](/rpc/usage-and-limits): Estimate credits and plan for shared rate and concurrency limits. --- # Supported methods Source: https://docs.graphops.xyz/rpc/methods GraphOps supports standard Ethereum-compatible JSON-RPC methods on supported EVM networks. Methods are available over HTTPS unless the transport column says WebSocket only. Current credit rates and method classes appear below. Bitcoin RPC uses native Bitcoin JSON-RPC method names rather than the EVM methods listed on this page. See [API integration](./api) for the Bitcoin endpoint and request shape. Method behavior can differ by network and node client. Check the notes for the methods you use; for an unlisted method or network-specific feature, ask [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) about availability. ## Choose a method * Use `eth_call`, `eth_getBalance`, `eth_getCode`, and `eth_getStorageAt` for direct state reads. * Use `eth_getLogs` for historical event backfills and WebSocket `logs` subscriptions for live event streams. * Use `eth_sendRawTransaction` for transaction submission. Sign transactions in your application or wallet. * Use `safe`, `finalized`, or explicit block references when consistency matters more than freshness. * Use [Substreams](/substreams) or [Subgraphs](/subgraphs) when the workflow is better served by streaming or indexed GraphQL access. ## Current credit rates Current rates as of Nov 12, 2026, 3:39 AM UTC. | Usage component | Applies to | Rate | | --- | --- | --- | | RPC calls | Default | 20 credits per RPC call | | RPC calls | RPC method cost class: Standard | 20 credits per RPC call | | RPC calls | RPC method cost class: Compute-heavy | 30 credits per RPC call | | RPC calls | RPC method cost class: Log scans | 40 credits per RPC call | | RPC calls | RPC method cost class: State proofs | 40 credits per RPC call | | RPC calls | RPC method cost class: Trace and debug | 60 credits per RPC call | | Subscription events | Default | 20 credits per subscription event | | Subscription events | Subscription type: newHeads | 20 credits per subscription event | | Subscription events | Subscription type: logs | 20 credits per subscription event | | Subscription events | Subscription type: newPendingTransactions | 20 credits per subscription event | | Subscription events | Subscription type: syncing | 20 credits per subscription event | | Response bytes | Default | 0 credits per GiB | | Response bytes | RPC method cost class: Log scans | 1,500,000 credits per GiB | ### RPC method cost class | Class | Methods | | --- | --- | | Compute-heavy | `eth_call`, `eth_createAccessList`, `eth_estimateGas` | | Log scans | `eth_getLogs` | | State proofs | `eth_getProof` | | Trace and debug | `debug_…`, `trace_…` | | Standard | All methods not matched by another class | ## Request example ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] } ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x191161d" } ``` JSON-RPC version Matches the id you sent Latest block in decimal: 26,285,597 ## Client and network | Method | Params | Returns | Transport | Notes | | --------------------- | -------- | ----------------------------- | --------- | ------------------------------------------------ | | `web3_clientVersion` | `[]` | Client version string | HTTPS | Useful for diagnostics, not feature detection. | | `web3_sha3` | `[data]` | Keccak-256 hash | HTTPS | Input and output are hex-encoded. | | `net_version` | `[]` | Network ID string | HTTPS | Prefer `eth_chainId` for EVM chain identity. | | `net_listening` | `[]` | Boolean | HTTPS | Compatibility method. | | `net_peerCount` | `[]` | Peer count hex quantity | HTTPS | Value depends on upstream node and network. | | `eth_chainId` | `[]` | EIP-155 chain ID hex quantity | HTTPS | Use before signing transactions. | | `eth_protocolVersion` | `[]` | Protocol version string | HTTPS | Legacy compatibility method. | | `eth_syncing` | `[]` | Sync object or `false` | HTTPS | Hosted endpoints should normally return `false`. | ## Blocks and fees | Method | Params | Returns | Transport | Notes | | -------------------------------------- | ---------------------------------------------- | ------------------------------ | --------- | ------------------------------------------------------------------------------------------ | | `eth_blockNumber` | `[]` | Latest known block number | HTTPS | Use as an anchor for repeatable reads. | | `eth_getBlockByHash` | `[blockHash, hydrateTransactions]` | Block object or `null` | HTTPS | Set hydration to `true` only when full transactions are needed. | | `eth_getBlockByNumber` | `[blockTagOrNumber, hydrateTransactions]` | Block object or `null` | HTTPS | Supports `latest`, `safe`, `finalized`, and explicit block numbers. | | `eth_getBlockReceipts` | `[blockHashOrNumber]` | Receipt array or `null` | HTTPS | More efficient than many individual receipt lookups when supported by the upstream client. | | `eth_getBlockTransactionCountByHash` | `[blockHash]` | Transaction count hex quantity | HTTPS | Returns `null` if the block is unknown. | | `eth_getBlockTransactionCountByNumber` | `[blockTagOrNumber]` | Transaction count hex quantity | HTTPS | Use explicit blocks for repeatable reads. | | `eth_gasPrice` | `[]` | Gas price hex quantity | HTTPS | For EIP-1559 transactions, prefer fee history and priority fee methods. | | `eth_maxPriorityFeePerGas` | `[]` | Priority fee hex quantity | HTTPS | Client and network behavior can vary. | | `eth_feeHistory` | `[blockCount, newestBlock, rewardPercentiles]` | Fee history object | HTTPS | Use bounded block counts. | | `eth_blobBaseFee` | `[]` | Blob base fee hex quantity | HTTPS | Relevant for networks with EIP-4844 blob gas. | ## State and execution | Method | Params | Returns | Transport | Notes | | ------------------------- | --------------------------------- | -------------------------------- | --------- | ------------------------------------------------------------------- | | `eth_getBalance` | `[address, block]` | Wei balance hex quantity | HTTPS | Use explicit blocks for repeatable reads. | | `eth_getStorageAt` | `[address, slot, block]` | 32-byte storage value | HTTPS | Archive access is available for historical state. | | `eth_getTransactionCount` | `[address, block]` | Account nonce hex quantity | HTTPS | Use `pending` carefully when preparing transactions. | | `eth_getCode` | `[address, block]` | Contract bytecode | HTTPS | Empty bytecode is returned for EOAs and nonexistent contracts. | | `eth_getProof` | `[address, storageKeys, block]` | Account and storage proof object | HTTPS | Useful for verification workflows. | | `eth_call` | `[transactionCallObject, block]` | Return data bytes | HTTPS | Contract reverts are application results unless the envelope fails. | | `eth_estimateGas` | `[transactionCallObject, block?]` | Estimated gas hex quantity | HTTPS | Treat as an estimate, not a guarantee. | | `eth_createAccessList` | `[transactionCallObject, block?]` | Access-list result object | HTTPS | Useful for advanced transaction preparation. | ## Transactions | Method | Params | Returns | Transport | Notes | | ----------------------------------------- | --------------------------- | ---------------------------- | --------- | ---------------------------------------------------------------------------- | | `eth_sendRawTransaction` | `[signedTransactionData]` | Transaction hash | HTTPS | Recommended submission path. Sign locally. | | `eth_sendTransaction` | `[transactionObject]` | Transaction hash | HTTPS | Hosted RPC does not custody user keys; most apps should not rely on this. | | `eth_sign` | `[address, data]` | Signature | HTTPS | Hosted RPC does not custody user keys; prefer wallet or application signing. | | `eth_signTransaction` | `[transactionObject]` | Signed transaction data | HTTPS | Hosted RPC does not custody user keys. | | `eth_getTransactionByHash` | `[transactionHash]` | Transaction object or `null` | HTTPS | Returns `null` until the transaction is known to the node. | | `eth_getTransactionByBlockHashAndIndex` | `[blockHash, index]` | Transaction object or `null` | HTTPS | Use when the block hash is already known. | | `eth_getTransactionByBlockNumberAndIndex` | `[blockTagOrNumber, index]` | Transaction object or `null` | HTTPS | Use explicit block numbers for repeatable reads. | | `eth_getTransactionReceipt` | `[transactionHash]` | Receipt object or `null` | HTTPS | Returns `null` while pending. | | `eth_accounts` | `[]` | Address array | HTTPS | Hosted RPC normally returns an empty list. | ## Logs and filters | Method | Params | Returns | Transport | Notes | | --------------------------------- | ---------------- | ------------------------------------------ | --------- | -------------------------------------------------------------------------- | | `eth_getLogs` | `[filterObject]` | Log array | HTTPS | Use bounded block ranges; broad archive scans can hit organization limits. | | `eth_newFilter` | `[filterObject]` | Filter ID | HTTPS | Store filter IDs only for short-lived polling. | | `eth_newBlockFilter` | `[]` | Filter ID | HTTPS | Prefer WebSocket `newHeads` for long-lived streams. | | `eth_newPendingTransactionFilter` | `[]` | Filter ID | HTTPS | Pending transaction visibility varies by network. | | `eth_getFilterChanges` | `[filterId]` | Log, block hash, or transaction hash array | HTTPS | Poll frequently enough to avoid filter expiration. | | `eth_getFilterLogs` | `[filterId]` | Log array | HTTPS | Use `eth_getLogs` for stateless historical reads. | | `eth_uninstallFilter` | `[filterId]` | Boolean | HTTPS | Always uninstall filters created by short-lived jobs. | ## WebSocket subscriptions | Method | Params | Returns | Transport | Notes | | ----------------- | ------------------------------ | --------------- | --------- | ------------------------------------------------------------------------------------ | | `eth_subscribe` | `[subscriptionType, options?]` | Subscription ID | WebSocket | Supported types include `newHeads`, `logs`, `newPendingTransactions`, and `syncing`. | | `eth_unsubscribe` | `[subscriptionId]` | Boolean | WebSocket | Unsubscribe before closing long-lived clients when possible. | ## Legacy compatibility These methods exist for compatibility with Ethereum clients and older tooling. Some return values are not useful on proof-of-stake or L2 networks. | Method | Params | Returns | Transport | Notes | | ----------------------------------- | --------------------------- | ---------------------------- | --------- | ------------------------------------------- | | `eth_coinbase` | `[]` | Address | HTTPS | Legacy compatibility method. | | `eth_mining` | `[]` | Boolean | HTTPS | Usually `false` on modern hosted endpoints. | | `eth_hashrate` | `[]` | Hashrate hex quantity | HTTPS | Usually `0x0` on modern hosted endpoints. | | `eth_getWork` | `[]` | Work array | HTTPS | Legacy proof-of-work method. | | `eth_submitWork` | `[nonce, powHash, digest]` | Boolean | HTTPS | Legacy proof-of-work method. | | `eth_submitHashrate` | `[hashrate, id]` | Boolean | HTTPS | Legacy proof-of-work method. | | `eth_getUncleCountByBlockHash` | `[blockHash]` | Uncle count hex quantity | HTTPS | Legacy Ethereum block structure. | | `eth_getUncleCountByBlockNumber` | `[blockTagOrNumber]` | Uncle count hex quantity | HTTPS | Legacy Ethereum block structure. | | `eth_getUncleByBlockHashAndIndex` | `[blockHash, index]` | Uncle block object or `null` | HTTPS | Legacy Ethereum block structure. | | `eth_getUncleByBlockNumberAndIndex` | `[blockTagOrNumber, index]` | Uncle block object or `null` | HTTPS | Legacy Ethereum block structure. | ## Next steps * [API integration](./api): Shape request envelopes, batches, and WebSocket connections. * [Read contract state](./guides/read-contract-state): Use `eth_call` at an explicit block. * [Fetch logs](./guides/fetch-logs): Use `eth_getLogs` with a bounded block range. --- # Supported networks Source: https://docs.graphops.xyz/rpc/networks Find the endpoint for your network, confirm it supports the transport you need, and check whether it serves archive data. ## Supported endpoints | Network | Network ID (CAIP-2) | HTTPS endpoint | WebSocket endpoint | Archive | | --- | --- | --- | --- | --- | | Ethereum Mainnet | eip155:1 | https://rpc.graphops.xyz/v1/evm/1 | wss://rpc.graphops.xyz/v1/evm/1 | Yes | | Ethereum Sepolia | eip155:11155111 | https://rpc.graphops.xyz/v1/evm/11155111 | wss://rpc.graphops.xyz/v1/evm/11155111 | Yes | | Gnosis | eip155:100 | https://rpc.graphops.xyz/v1/evm/100 | wss://rpc.graphops.xyz/v1/evm/100 | Yes | | Base | eip155:8453 | https://rpc.graphops.xyz/v1/evm/8453 | wss://rpc.graphops.xyz/v1/evm/8453 | Yes | | Arbitrum One | eip155:42161 | https://rpc.graphops.xyz/v1/evm/42161 | wss://rpc.graphops.xyz/v1/evm/42161 | Yes | | Arbitrum Sepolia | eip155:421614 | https://rpc.graphops.xyz/v1/evm/421614 | wss://rpc.graphops.xyz/v1/evm/421614 | Yes | | Bitcoin Mainnet | bip122:000000000019d6689c085ae165831e93 | https://rpc.graphops.xyz/v1/bitcoin/mainnet | — | — | | Bitcoin Signet | bip122:00000008819873e925422c1ff0f99f7c | https://rpc.graphops.xyz/v1/bitcoin/signet | — | — | This catalog includes currently published mainnet and testnet endpoints. Copy the generated endpoint for your network when configuring a connection. The CAIP-2 ID identifies the network; the endpoint contains its service-specific routing reference. ## EVM endpoints The EVM RPC endpoint path ends in the EVM chain ID: ```text https://rpc.graphops.xyz/v1/evm/ wss://rpc.graphops.xyz/v1/evm/ ``` ## Bitcoin endpoints Bitcoin RPC uses HTTPS and a network name instead of an EVM chain ID: ```text https://rpc.graphops.xyz/v1/bitcoin/ ``` Choose a Bitcoin network from the catalog above. ## Network notes | Network | Notes | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Ethereum Mainnet | Use `safe`, `finalized`, or explicit block references when user-facing workflows need more stability than `latest`. See [Consistency and finality](./concepts/consistency-and-finality). | | Arbitrum One | L2 transaction visibility and finality can differ from Ethereum L1. Use explicit block references for multi-call workflows. | | Base | L2 transaction visibility and finality can differ from Ethereum L1. Use explicit block references for multi-call workflows. | ## Archive support Check the Archive column above for each network’s published capability. **Yes** means historical state, such as `eth_call` at a past block, and historical event logs are available; see [Archive reads](./concepts/archive-reads). Historical state reads and broad log backfills are heavier than current-state reads. Use bounded ranges and split large jobs into resumable chunks. ## Next steps * [Authentication](./authentication): Add your API key as a header or route token. * [Consistency and finality](./concepts/consistency-and-finality): Choose `latest`, `safe`, `finalized`, or a stored block for each read. * [Archive reads](./concepts/archive-reads): Plan historical reads on networks with archive coverage. --- # Troubleshooting Source: https://docs.graphops.xyz/rpc/troubleshooting Find the error or symptom you see, then work through its checks in order. Each entry names the likely cause first. If the checks do not resolve the issue, contact support with the details listed in [Need support](#need-support). ## `401 Unauthorized` The request is missing a valid API key, the `Authorization` header is malformed, or the route-token URL does not include a valid key. 1. Confirm the key was created for the organization that owns RPC access. 2. For header auth, confirm the header is exactly `Authorization: Bearer `. 3. For route-token auth, confirm the endpoint ends with `/` after the EVM chain ID or Bitcoin network name. 4. Confirm the key has not been revoked or rotated. 5. Remove extra quotes, whitespace, or the literal string `Bearer` from secret-manager values. 6. Use [Authentication](./authentication) to compare header and route-token formats. ## `403 Forbidden` The key is valid, but service access for the organization is paused, usually because its credit balance reached zero. 1. Check your credit balance and whether service access is paused in **Billing**. 2. Add credits to bring the balance above zero. Access is restored automatically. To avoid a repeat, turn on [automatic top-ups](/platform/billing-and-credits#automatic-top-ups). 3. If GraphOps support paused the organization, adding credits does not restore access. Contact [support in the GraphOps Platform](https://platform.graphops.xyz/organizations/current/overview?support=open). ## `429 Too Many Requests` The organization exceeded its rate or concurrency limit. 1. Reduce parallel requests. 2. Add exponential backoff with jitter. 3. Split large batch requests into smaller batches. 4. Move historical backfills to lower-concurrency workers. 5. Request a limit increase through [support in the GraphOps Platform](https://platform.graphops.xyz/organizations/current/overview?support=open). ## Timeouts or large `eth_getLogs` failures The log query is too broad, the response is too large, or the archive read takes longer than the request timeout. 1. Reduce the block range. 2. Filter by contract address and topic whenever possible. 3. Persist progress and backfill in deterministic block chunks. 4. Avoid retrying the same broad range repeatedly. 5. Use WebSocket `logs` subscriptions for live logs and `eth_getLogs` for backfill. ## Inconsistent results across requests Separate `latest` reads can observe different blocks as the chain advances. 1. Read a block number or block hash first. 2. Pass that explicit block reference to subsequent state reads. 3. Use `safe` or `finalized` when the workflow can accept additional latency. 4. Avoid mixing `latest` reads with historical reads in the same consistency-sensitive workflow. 5. Use [Consistency and finality](./concepts/consistency-and-finality) for the repeatable-read pattern. ## `method not found` The request method name is misspelled, sent to the wrong endpoint, or is outside the standard Ethereum-compatible method set for the selected network. 1. Confirm the method name uses the exact `eth_*`, `net_*`, or `web3_*` casing. 2. Confirm the request is sent to an RPC endpoint, not the platform REST API. 3. Confirm the method is listed in [Supported methods](./methods). 4. For WebSocket subscriptions, confirm the method is `eth_subscribe` and the endpoint is a WebSocket URL. ## Transaction submission errors Transaction errors usually come from EVM validation or mempool policy rather than from GraphOps itself. 1. Confirm the transaction was signed for the same chain ID as the endpoint. 2. Confirm the sender has enough funds for value plus gas. 3. Poll `eth_getTransactionReceipt` when the same transaction hash may already be known. 4. Use [Send a raw transaction](./guides/send-raw-transaction) for nonce, replacement, and retry guidance. ## WebSocket authentication fails The WebSocket URL is missing the route token, the token is invalid, or the client is trying to use a header-based pattern that it does not support. 1. Prefer the route-token endpoint: `wss://rpc.graphops.xyz/v1/evm//`. 2. Confirm the API key is present after the chain ID and does not include the literal `Bearer` prefix. 3. If you use header authentication, confirm the WebSocket client can send custom headers during the handshake. 4. Redact route-token URLs from logs before sharing diagnostics. 5. Use [Authentication](./authentication) for WebSocket auth patterns and security guidance. ## WebSocket stream gaps Connections can disconnect, subscriptions can expire, and live streams can miss events while a client is offline. 1. Store the last processed block or log position. 2. Reconnect with backoff. 3. Resubscribe after reconnect. 4. Backfill missed logs with `eth_getLogs`. 5. Deduplicate logs by `(blockHash, transactionHash, logIndex)`. 6. Use [WebSocket subscriptions](./guides/websocket-subscriptions#recover-after-a-disconnect) for the full recovery sequence. ## Empty `eth_accounts` Hosted RPC does not custody user private keys. Account-management methods are present for client compatibility, but applications should sign with a wallet, signer service, or local key-management system. 1. Sign transactions in the wallet or application. 2. Submit signed transactions with `eth_sendRawTransaction`. 3. Do not rely on `eth_sendTransaction`, `eth_sign`, or `eth_signTransaction` for hosted key management. ## Need support When contacting [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open), include: * Organization name or ID. * Network and endpoint. * Transport: HTTPS or WebSocket. * RPC method. * Approximate timestamp. * JSON-RPC error object or HTTP status. * Block number, block range, or transaction hash if relevant. * Whether the request used `latest`, `safe`, `finalized`, or an explicit block reference. ## Next steps * [Authentication](./authentication): Compare header and route-token formats for your client. * [Retries and reorgs](./guides/retries-and-reorgs): Decide which errors to retry and how to recover from reorgs. * [Usage and limits](./usage-and-limits): Check the limits behind `429` and timeout errors. --- # Usage and limits Source: https://docs.graphops.xyz/rpc/usage-and-limits Estimate what your RPC workload will cost and keep it within your organization's shared limits. RPC calls and WebSocket subscription events draw credits at the rates for their method class. Rate and concurrency limits apply to your whole organization. ## Current credit rates Use the method rates below to estimate request costs. Track actual consumption in [Platform Usage](/platform/usage-and-limits). Current rates as of Nov 12, 2026, 3:39 AM UTC. | Usage component | Applies to | Rate | | --- | --- | --- | | RPC calls | Default | 20 credits per RPC call | | RPC calls | RPC method cost class: Standard | 20 credits per RPC call | | RPC calls | RPC method cost class: Compute-heavy | 30 credits per RPC call | | RPC calls | RPC method cost class: Log scans | 40 credits per RPC call | | RPC calls | RPC method cost class: State proofs | 40 credits per RPC call | | RPC calls | RPC method cost class: Trace and debug | 60 credits per RPC call | | Subscription events | Default | 20 credits per subscription event | | Subscription events | Subscription type: newHeads | 20 credits per subscription event | | Subscription events | Subscription type: logs | 20 credits per subscription event | | Subscription events | Subscription type: newPendingTransactions | 20 credits per subscription event | | Subscription events | Subscription type: syncing | 20 credits per subscription event | | Response bytes | Default | 0 credits per GiB | | Response bytes | RPC method cost class: Log scans | 1,500,000 credits per GiB | ### RPC method cost class | Class | Methods | | --- | --- | | Compute-heavy | `eth_call`, `eth_createAccessList`, `eth_estimateGas` | | Log scans | `eth_getLogs` | | State proofs | `eth_getProof` | | Trace and debug | `debug_…`, `trace_…` | | Standard | All methods not matched by another class | Archive reads can consume more credits when their method is assigned to a higher-cost class in the current catalog. Check [Supported networks](./networks#supported-endpoints) for archive coverage on your network. ## Organization limits These limits apply to your whole organization, not to each API key, and they do not change with your plan size. [Contact support](/platform/support-and-status) for the values that apply to your workload or to request an increase. | Limit | Applies to | Guidance | | ----------------------- | ----------------------------------------------- | ------------------------------------------------------------ | | Request rate | HTTPS and WebSocket RPC calls | Spread traffic across time and use backoff after `429`. | | Concurrent requests | In-flight requests for the organization | Bound worker pools and queue background jobs. | | Batch size | Number of JSON-RPC calls in one HTTP batch | Keep batches small enough to retry individual failures. | | Response size | Large logs, blocks, receipts, and archive reads | Use narrower ranges or paginated application workflows. | | Request timeout | Long state reads and log scans | Split large archive jobs by block range. | | WebSocket connections | Live subscriptions | Reuse connections and unsubscribe unused streams. | | WebSocket subscriptions | Active subscriptions per organization | Prefer one filtered subscription over many overlapping ones. | ## Rate-limit and request headers > **Treat optional headers as informational** > > Do not rely on rate-limit or request-tracing headers to enforce production limits. Handle `429` > responses, use bounded concurrency, apply backoff with jitter, and monitor usage in the platform UI. Responses may include headers such as: ```http X-GraphOps-RateLimit-Limit: X-GraphOps-RateLimit-Remaining: X-GraphOps-RateLimit-Reset: X-GraphOps-Request-Id: ``` These headers are informational; availability can vary by endpoint. ## Control your usage Most limit errors come from a few workload shapes. Keep each one bounded: | Workload | Keep it within limits | Guide | | --------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Current-state reads | Retry `429`, `502`, `503`, and `504` with exponential backoff and jitter, and cap total retry time. | [Retries and reorgs](./guides/retries-and-reorgs) | | Log scans and archive reads | Split work into block ranges and reduce the range before retrying. | [Backfill historical events](./guides/backfill-historical-events) | | Batch requests | Keep batches small, match responses by `id`, and retry only failed retryable items. | [API integration](./api#batch-requests) | | Transaction submission | Retry the same signed transaction; never change it and resend blindly. | [Send a raw transaction](./guides/send-raw-transaction) | | WebSocket streams | Reuse connections, unsubscribe unused streams, and backfill gaps after a reconnect. | [WebSocket subscriptions](./guides/websocket-subscriptions) | ## Production checklist * Store API keys outside source code. See [Authentication](./authentication). * Bound request concurrency per organization. * Add exponential backoff with jitter for retryable errors. * Use [explicit block references](./concepts/consistency-and-finality) for multi-call workflows. * Split broad `eth_getLogs` and archive reads into block ranges. * Track credit usage in the platform UI. * Use route-token authentication for WebSocket clients that cannot send custom headers. * Contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) when limits need to be raised. ## Next steps * [Usage across your organization](/platform/usage-and-limits): Track credit consumption and confirm the limit values for your organization. * [Retries and reorgs](./guides/retries-and-reorgs): Retry failed requests without multiplying your request volume. * [Troubleshooting](./troubleshooting#429-too-many-requests): Diagnose `429 Too Many Requests` responses. --- # API integration Source: https://docs.graphops.xyz/substreams/api Look up what a Substreams client sends and receives. GraphOps Substreams exposes standard Substreams gRPC endpoints, so standard Substreams tooling works with a GraphOps endpoint URL and API key. This page lists the stream inputs, the messages a consumer handles, and where the upstream protocol reference lives. ## Endpoint patterns ```text https://.evm.substreams.graphops.xyz ``` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://1.evm.substreams.graphops.xyz x-api-key: ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=substreams&networkId=eip155%3A1) [Supported networks](/substreams/networks) Use [Supported networks](/substreams/networks) for supported chains and testnet availability. ## Transport Substreams uses gRPC streams. A client selects: | Field | Meaning | | -------------- | -------------------------------------------------------------------------------------- | | Endpoint | GraphOps chain endpoint. | | Package | Public package name, package URL, local `.spkg`, or other standard package reference. | | Module | Output module to stream. | | Start block | First block to process. | | Stop block | Optional end block or relative range. | | Cursor | Optional resume point from a previous stream. | | Authentication | `SUBSTREAMS_API_KEY` for CLI, or `x-api-key: ` for raw gRPC clients. | ## API versions Every GraphOps Substreams endpoint serves the v2, v3, and v4 streaming APIs with the same API key. Use the version your client library implements. | Version | gRPC method | Request | Response | | ------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | v2 | `sf.substreams.rpc.v2.Stream/Blocks` | The client sends the module definitions from the package. | One `BlockScopedData` message per block. | | v3 | `sf.substreams.rpc.v3.Stream/Blocks` | The client sends the full `.spkg` package. The server applies module parameter overrides (`params`) and selects the package network (`network`). | Same as v2. | | v4 | `sf.substreams.rpc.v4.Stream/Blocks` | Same as v3. | `BlockScopedDatas`, a batch of consecutive `BlockScopedData` messages in block order. Other messages are the same as v2. | The Firehose services that also appear in an endpoint's service description are not available with a GraphOps API key. ## Request The CLI tab is a complete command. The TypeScript, Go, Rust, and Python tabs are complete consumers of the v2 API. Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.api.sh-2.tar.gz) ```sh # The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header. export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}" # Stream USDC Transfer events (--params) for six blocks from 18000002 as JSON lines. 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 +6 \ -o jsonl ``` Output ```json { "@module": "filtered_events", "@block": 18000002, "@type": "sf.substreams.ethereum.v1.Events", "@data": { "clock": { "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "number": "18000002", "timestamp": "2023-08-26T16:21:59Z" }, "events": [ { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "index": 1, "blockIndex": 172, "ordinal": "2582" }, "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d" }, { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "index": 2, "blockIndex": 315, "ordinal": "5139" }, "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db" } ] } } { "@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,002 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 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 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 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> { 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 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 ## Response A stream sends these standard Substreams messages. A v4 stream wraps `BlockScopedData` in `BlockScopedDatas` batches; handle each item in order. | Message | Meaning | Consumer action | | ------------------------- | -------------------------------------------------- | --------------------------------------------- | | `SessionInit` | Stream session metadata. | Log the session and continue. | | `BlockScopedData` | Output for one block and a cursor. | Commit the output, then persist the cursor. | | `BlockUndoSignal` | Reorg correction with last valid block and cursor. | Revert local state to the last valid point. | | `ModulesProgress` | Progress and processed-byte metadata. | Use for logs, metrics, and backfill progress. | | `InitialSnapshotData` | Snapshot data for supported sink flows. | Process before live or range output. | | `InitialSnapshotComplete` | Snapshot completion marker. | Mark snapshot ingestion complete. | ## Authentication See [Authentication](/substreams/authentication) for API-key creation, `x-api-key` headers, and secret handling. Route-token authentication is not supported for Substreams. ## Standard reference GraphOps supports standard Substreams behavior. Use upstream Substreams documentation for protocol, package, manifest, module, and sink details: * [Substreams documentation](https://docs.substreams.dev/) * [Substreams package registry](https://substreams.dev/packages) * [Substreams core concepts](https://docs.substreams.dev/reference-material/core-concepts) * [Substreams manifest and components](https://docs.substreams.dev/reference-material/manifest-and-components) * [Substreams gRPC protocol definitions](https://github.com/streamingfast/substreams/tree/develop/proto/sf/substreams/rpc) ## Next steps * [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs): Decide how your consumer commits cursors and handles undo signals. * [Packages and modules](/substreams/concepts/packages-and-modules): Choose the package and output module a stream runs. * [Sinks and materialization](/substreams/concepts/sinks-and-materialization): Pick a standard sink for your destination. --- # Authentication Source: https://docs.graphops.xyz/substreams/authentication Authenticate every Substreams stream with an organization API key. The Substreams CLI and sinks read the key from the `SUBSTREAMS_API_KEY` environment variable; raw gRPC clients send it in the `x-api-key` request metadata. The key never goes in the endpoint URL. _One API key, two ways to send it. Neither puts the key in the endpoint URL, so a copied or logged URL never exposes it._ - `SUBSTREAMS_API_KEY`: CLI and sinks - `x-api-key`: Raw gRPC metadata - Endpoint: `https://.evm.substreams.graphops.xyz` (No key in the URL) ## Create and rotate API keys Create a Substreams API key in the platform. The platform shows the plaintext key only when you create or rotate it, so store it in your secret manager immediately. Each key belongs to one organization and product. Use separate keys for production, staging, and local development so you can replace one environment's credentials without affecting the others. [Create or rotate a Substreams key](https://platform.graphops.xyz/organizations/current/api-keys) ## Send the key | Pattern | Supported | Use it for | | ------------------------------- | --------- | -------------------------------------------------------------------------------- | | `SUBSTREAMS_API_KEY` | Yes | The Substreams CLI, including `substreams sink` commands. | | `x-api-key: ` | Yes | Raw gRPC clients that attach request metadata. | | Route-token URL | No | Do not put Substreams API keys in the endpoint URL. | | Query-string API key | No | Do not put Substreams API keys in query parameters. | | mTLS | No | Use `x-api-key` authentication unless your enterprise contract states otherwise. | ### Substreams CLI Export the key, then run any `substreams` command against a GraphOps endpoint: Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.authentication.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}" # Stream USDC Transfer events (--params) for block 18000002 as JSON lines. 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 +1 \ -o jsonl ``` ### Raw gRPC clients Attach the key as request metadata on every stream: ```http x-api-key: ``` ## Protect the key * Store API keys in a server-side secret manager. * Do not commit API keys to Substreams manifests, sink configs, shell history, or source code. * Redact `x-api-key` headers from logs, metrics, traces, and support output. * Use separate keys per environment and rotate a key as soon as it is exposed. * Revoke or replace keys in **API keys**. Ask an Owner or Developer if you lack permission. If a stream is rejected, follow the checks in [Authentication fails](/substreams/troubleshooting#authentication-fails). ## Next steps * [Supported networks](/substreams/networks): Copy the endpoint for your network. * [API integration](/substreams/api): Check the stream fields and messages your client handles. * [Troubleshooting](/substreams/troubleshooting): Resolve rejected streams and other common failures. --- # Stream your first events Source: https://docs.graphops.xyz/substreams/getting-started Stream USDC Transfer events to see the output your consumer will receive. This quickstart runs the `filtered_events` module of the `ethereum-common` package with the Substreams CLI. The command pins `v0.4.0` and filters USDC Transfer events over a finite historical range. **Expected result:** the nine USDC Transfer events in blocks `[18000002, 18000008)`, printed as JSON lines. ## Prerequisites * A GraphOps Platform account. * An organization with access to Substreams. * An Owner or Developer role to create a Substreams API key, or a key supplied by your team. ### Create an API key \[#create-an-api-key] [Create a Substreams API key](https://platform.graphops.xyz/organizations/current/api-keys) Choose your organization, create the key for Substreams, and store the plaintext value in your secret manager. ### Choose the endpoint \[#choose-the-endpoint] Use the Ethereum mainnet endpoint: ```text https://1.evm.substreams.graphops.xyz ``` Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://1.evm.substreams.graphops.xyz x-api-key: ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=substreams&networkId=eip155%3A1) [Supported networks](/substreams/networks) ### Stream matching events \[#stream-matching-events] Run `filtered_events` from the public `ethereum-common` package: Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.api.sh-2.tar.gz) ```sh # The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header. export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}" # Stream USDC Transfer events (--params) for six blocks from 18000002 as JSON lines. 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 +6 \ -o jsonl ``` Output ```json { "@module": "filtered_events", "@block": 18000002, "@type": "sf.substreams.ethereum.v1.Events", "@data": { "clock": { "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "number": "18000002", "timestamp": "2023-08-26T16:21:59Z" }, "events": [ { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "index": 1, "blockIndex": 172, "ordinal": "2582" }, "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d" }, { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "index": 2, "blockIndex": 315, "ordinal": "5139" }, "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db" } ] } } { "@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,002 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 ### Inspect the output \[#inspect-the-output] The output covers blocks `[18000002, 18000008)`, one line per block; block `18000006` has no matching events. The stream stops at the stop block; remove `--stop-block` to follow the chain. ### Move toward production \[#move-toward-production] A production consumer saves each block's cursor in the same transaction as its output, then resumes from the latest committed cursor after reconnecting. ## Next steps * [Resume with cursors](/substreams/guides/resume-with-cursors): Commit output with its cursor and restart without skipping blocks. * [Stream USDC transfers](/substreams/guides/stream-usdc-transfers): Pass a module parameter to stream one contract's events. * [Authentication](/substreams/authentication): Store and send the API key safely in a long-running consumer. --- # GraphOps Substreams Source: https://docs.graphops.xyz/substreams Feed blockchain data into your own database, analytics pipeline, or event processor. GraphOps Substreams runs standard Substreams packages and delivers their output over gRPC, so you can backfill historical blocks and continue with a live stream. ## Substreams at a glance _GraphOps runs your package over each block and streams module output to your app, which saves a cursor to resume from._ - Your app: Cursor - Substreams: Modules - Chain: New blocks - [Run the Substreams quickstart](/substreams/getting-started) - [Open the Substreams reference](/substreams/api) ## Where to start - [Stream USDC Transfer events](/substreams/getting-started): Create an API key and stream USDC Transfer events from a standard package. - [Backfill a bounded range](/substreams/guides/backfill-a-range): Replay deterministic package output and commit it to your own system. - [Recover without losing work](/substreams/concepts/cursors-and-reorgs): Persist committed cursors and process undo signals after reversible blocks. ## How a Substreams pipeline is split _GraphOps runs your pinned package and streams its output from historical blocks through to the chain head. Everything after the stream runs in your infrastructure._ - Package: `name@version`, output module - GraphOps runs: endpoint (one per network), history (parallel execution), live (new blocks as they arrive) - You run: consumer (your client or a standard sink), commit (output and cursor together), undo (revert after a reorg), and your database GraphOps runs the package and delivers its output. Everything after the stream belongs to your consumer: decoding the output type, writing it to storage, saving cursors, and reverting writes after a reorg. ## What you can build * **Historical backfills that continue live.** Start at the block your history needs and leave the stop block open to keep following the chain. See [Backfill a range](/substreams/guides/backfill-a-range). * **Consumers that preserve matching output.** Commit each received output with its cursor and resume from that cursor after a restart. See [Resume with cursors](/substreams/guides/resume-with-cursors). * **State that stays correct through reorgs.** Revert writes after the last valid block when an undo signal arrives. See [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs). * **Package output in tables you own.** Run a standard sink that writes to Postgres, ClickHouse, files, or Pub/Sub. See [Run a sink](/substreams/guides/run-a-sink). ## Choose Substreams when Use Substreams when you need to process blocks in order, replay a range, or build a data model in your own storage. You choose the package and output module; your consumer commits the data and saves its progress. Use another GraphOps service when your job is different: | Need | Use | | -------------------------------------------------------------------- | ----------------------- | | Direct node calls, transactions, or contract state reads | [RPC](/rpc) | | Query indexed GraphQL entities without running your own materializer | [Subgraphs](/subgraphs) | ## Choose a package and destination Find reusable packages on [substreams.dev](https://substreams.dev/packages) or in [Discover Substreams](/substreams/guides/discover-packages), or use your own `.spkg` file. Pin the package version, choose an output module that produces the data your application needs, and connect it to a [supported network](/substreams/networks). Your consumer can write to SQL, files, queues, or a custom store. If you want GraphOps to host the sink, contact support about enterprise options. ## Plan for production Backfills and live streaming use the same stream protocol. Before you rely on stored data: * Save each cursor in the same transaction as the output it follows. * Handle undo signals, or stream final blocks only if you cannot revert writes. * Size backfills and reconnect loops to your organization's limits. Usage follows the billable components and rates on [Usage and limits](/substreams/usage-and-limits). ## Next steps * [Stream your first events](/substreams/getting-started): Stream USDC Transfer events with the Substreams CLI. * [Discover Substreams packages](/substreams/guides/discover-packages): Find a package and pin the release you will run. * [Supported networks](/substreams/networks): Copy the endpoint for the chain your package reads. --- # Supported networks Source: https://docs.graphops.xyz/substreams/networks Pick the endpoint for the chain your package reads. Each GraphOps Substreams endpoint serves one network, and its hostname starts with that network's decimal EVM chain ID. Endpoint preview. Replace placeholders with your identifier and API key before making requests. ```text https://1.evm.substreams.graphops.xyz x-api-key: ``` [Get query endpoint](https://platform.graphops.xyz/organizations/current/overview/endpoint?product=substreams&networkId=eip155%3A1) [Supported networks](/substreams/networks) ## Supported endpoints | Network | Network ID (CAIP-2) | gRPC endpoint | | --- | --- | --- | | Ethereum Mainnet | eip155:1 | https://1.evm.substreams.graphops.xyz | | Base | eip155:8453 | https://8453.evm.substreams.graphops.xyz | This catalog lists the currently published mainnet and testnet endpoints. Copy the generated endpoint for your network. The CAIP-2 ID identifies the network in the catalog; the hostname uses the decimal chain ID: ```text https://.evm.substreams.graphops.xyz ``` _A Substreams endpoint names the network in its hostname: the EVM chain ID, then the namespace. This example is Base (8453). Send the API key in the x-api-key header._ ```text https://8453.evm.substreams.graphops.xyz x-api-key: ``` - EVM chain ID: `8453` - Namespace: `evm` - Substreams host: `substreams.graphops.xyz` ## Check package compatibility A network in the catalog does not make every package work on it. Before you run a package against a network: * Confirm the package was built for that chain's block model. Its package page lists the network it declares. * Check module assumptions such as contract addresses and start blocks, which differ by network. Backfills and live streaming are available on every supported network, with standard Substreams cursor and reorg behavior. ## Next steps * [Getting started](/substreams/getting-started): Stream USDC Transfer events from Ethereum mainnet. * [Discover Substreams packages](/substreams/guides/discover-packages): Find packages that declare your network. * [Backfill a range](/substreams/guides/backfill-a-range): Load a network's history into your database. --- # Troubleshooting Source: https://docs.graphops.xyz/substreams/troubleshooting Find the symptom your stream or sink shows, then work through its checks in order. Each entry names the likely cause first. If the checks do not resolve it, contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) with the details listed at the end of this page. ## Authentication fails The stream is missing a valid API key, or the key is sent in the wrong place. 1. Confirm the key was created for the organization that owns Substreams access. 2. Confirm the key has not been revoked or rotated. 3. For the CLI and sinks, confirm `SUBSTREAMS_API_KEY` is exported in the shell that runs the command. 4. For raw gRPC clients, confirm every stream sends `x-api-key: ` metadata. 5. Remove extra quotes or whitespace from the stored secret value. 6. Compare your setup with [Authentication](/substreams/authentication). ## Package cannot be resolved Standard Substreams tooling cannot load the package name, version, URL, or local `.spkg` reference. 1. Confirm the package exists on [substreams.dev](https://substreams.dev/packages) or at the URL you provided. 2. Pin an exact package version rather than a moving tag such as `latest`. 3. Confirm an unpublished `.spkg` file is readable by the process running the consumer. 4. Confirm the package supports the network endpoint you selected. ## Module not found The package does not expose the requested output module. 1. Check the package page or manifest for the available modules. 2. Match the module name exactly, including case. 3. Confirm the module is a map module you can stream, not a store module. 4. Regenerate client Protobuf types if the package output changed. ## Stream disconnects Long-running gRPC streams can disconnect because of network, client, or service conditions. 1. Reconnect with exponential backoff and jitter. 2. Resume from the latest committed cursor. 3. Confirm the cursor is stored durably, not only in process memory. 4. Reduce concurrency if many consumers reconnect at once. 5. Log the endpoint, package reference, module name, cursor, and block number for each disconnect. ## Duplicate data after reconnect The consumer resumed from an earlier cursor, or replayed a block after a failed commit. 1. Make output writes idempotent. 2. Save the cursor only after the output commits. 3. Store the cursor and output in one transaction when possible. 4. Deduplicate by block number, block hash, ordinal, or a package-specific ID. See [Resume with cursors](/substreams/guides/resume-with-cursors) for the commit order. ## Undo signal not handled The stream sent a `BlockUndoSignal`, but the consumer did not revert its local state. 1. Implement undo handling before you use reversible data for production state. 2. Revert local state to the last valid block named in the signal. 3. Save the signal's cursor after the rollback. 4. Keep enough local history to undo recent writes. See [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs) for the recovery model. ## Backfill is slow or expensive The range is large, the output volume is high, or concurrency exceeds your organization's limits. 1. Split the backfill into smaller ranges you can retry independently. 2. Reduce concurrency. 3. Use a narrower output module when possible. 4. Track blocks processed, bytes out, bytes read, and bytes written. 5. Check rates and limits in [Usage and limits](/substreams/usage-and-limits). ## You need GraphOps to host the sink Self-service Substreams assumes you run the sink. Hosted sinks are an enterprise option. 1. Check whether a standard sink in your infrastructure fits; see [Run a sink](/substreams/guides/run-a-sink). 2. If not, contact GraphOps from the platform about hosted sink options. ## Need support When you contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open), include: * Organization name or ID. * Endpoint URL and chain ID. * Package reference and version. * Module name. * Start block, stop block, and latest committed cursor. * Approximate timestamp. * gRPC status or error message. * Whether the consumer saw `BlockScopedData`, `BlockUndoSignal`, or progress messages. Never include your API key. ## Next steps * [Resume with cursors](/substreams/guides/resume-with-cursors): Make restarts safe so disconnects do not lose or duplicate data. * [Backfill a range](/substreams/guides/backfill-a-range): Plan a large history as bounded, retryable ranges. * [Usage and limits](/substreams/usage-and-limits): Check the limits that slow or reject streams. --- # Usage and limits Source: https://docs.graphops.xyz/substreams/usage-and-limits Budget Substreams work before a large backfill or a new live consumer. Streams draw credits from your organization's balance at the rates below, and limits on concurrent work are shared across your organization. ## Current credit rates Estimate stream costs from these billable components, then track actual consumption in [Platform Usage](/platform/usage-and-limits). For GraphOps-hosted sinks, contact support about enterprise pricing. Current rates as of Nov 12, 2026, 3:39 AM UTC. | Usage component | Applies to | Rate | | --- | --- | --- | | Processed blocks | Default | 4,000 credits per 1,000 processed blocks | | Egress bytes | Default | 375,000 credits per GiB | ## Organization limits These limits apply to your whole organization, not to each API key, and they do not change with your plan size. [Contact support](/platform/support-and-status) for the values that apply to your workload or to request an increase. | Limit | Applies to | Guidance | | ------------------ | ---------------------------------------- | ----------------------------------------------------------- | | Concurrent streams | Active gRPC streams for the organization | Bound consumer processes and reconnect loops. | | Worker concurrency | Parallel package execution work | Tune backfills to stay within your organization's capacity. | | Output throughput | Bytes sent to consumers | Reduce selection/output volume or split ranges. | | Message size | Large block-scoped outputs | Use narrower modules or package outputs when possible. | | Backfill pressure | Large historical ranges | Chunk ranges and checkpoint progress. | ## Control your usage * **Bound concurrency.** Cap consumer processes and reconnect loops so a restart does not open extra streams. * **Retry with backoff.** Retry transient failures with exponential backoff and jitter, then resume from the latest committed cursor. If a stream fails repeatedly at the same block, check the package output, module parameters, and message size. * **Narrow the output.** Choose an output module that emits only the data you store; output volume counts toward throughput limits. * **Split large backfills.** Run bounded ranges you can retry independently, and save progress after each one. * **Monitor the work.** Track blocks processed, bytes out, bytes read, bytes written, stream lag, retries, and errors. Contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open) when a limit needs to be raised. See [Platform: Usage and limits](/platform/usage-and-limits) for billing-window timing and how to confirm your limit values. ## Next steps * [Backfill a range](/substreams/guides/backfill-a-range): Split a large history into ranges that fit your limits. * [Resume with cursors](/substreams/guides/resume-with-cursors): Reconnect without replaying work you already committed. * [Troubleshooting](/substreams/troubleshooting): Diagnose slow backfills and repeated disconnects. --- # Secure your account Source: https://docs.graphops.xyz/platform/account-and-security Protect your GraphOps account and keep a working way to sign in when you replace a device or disconnect a provider. Your account holds your personal sign-in methods and sessions; organization roles control what you can do in each team. _You sign in with a primary method, add a second factor so a password or passkey alone is not enough, and get a session on each browser or device. GraphOps prevents removal of your last usable sign-in method._ - Sign in with: Password, Passkey, Google or GitHub. - Second factor: Authenticator app, Security key. - Sessions: One per browser or device, End any of them from Account. - Keep another working sign-in method before removing one. ## Create an account or sign in [Create account](https://platform.graphops.xyz/auth/register) [Sign in](https://platform.graphops.xyz/auth/start?mode=login) Choose email or an available Google or GitHub sign-in option. Provider options depend on which methods are enabled. Complete any requested profile details and email verification before continuing to your organization. If GraphOps finds an existing account for your email, follow the sign-in and connection prompts to prove that account belongs to you. Review the account shown before confirming a connection. If you are accepting a team invitation, use the account with the invited email address. ### Find your verification email The GraphOps verification email contains a verification code and a link. Enter the code on the page where you started, or open the link. Both expire at the time shown in the email. Check your inbox and spam or junk folder for the GraphOps verification email. If it was marked as spam, mark it as **Not spam**. Choose \**Haven’t received it?*\* on the verification page to resend, then use the code or link from the latest email. When you start with Google or GitHub, finish sign-in in the browser where you began. A verification link opened elsewhere can verify the email, but you still need the original browser to continue that sign-in attempt. If the attempt has expired, start sign-in again. ## Open your account settings Choose **Account** from the sidebar or your account menu. You can manage your personal sign-in methods even before joining an organization. Account has three sections: **Sign-in methods**, **Two-factor authentication** and **Sessions**. [Account](https://platform.graphops.xyz/account) Available methods show whether they are configured or disabled by policy. Complete the sign-in check when prompted before changing credentials, connecting a provider, or removing a method. After returning to **Account**, check that the method's status or entry reflects your change. ## Set a password or add a passkey Under **Sign-in methods**: * Choose **Set up password** or **Change password** below **Password**, then follow the sign-in and password prompts. * Choose **Add passkey** and follow your browser or device's enrollment prompts. After returning, check that the named passkey is ready before relying on it. You can sign in with a passkey instead of your password. A passkey replaces your password only: when you have set up [two-factor authentication](#add-two-factor-authentication), GraphOps also asks for your second factor after a passkey sign-in. If setup was interrupted and **Check setup** appears, use it to confirm the previous result before starting another setup. If you cannot sign in with your password, use password recovery from the sign-in page. ## Connect Google or GitHub Connecting a provider lets you use it to sign in to the same GraphOps account. 1. In **Account**, find **Connected accounts**. 2. Choose the available **Connect Google** or **Connect GitHub** action. 3. Sign in to the intended provider account, complete the requested GraphOps account verification, and confirm the connection. 4. Check that the provider and account appear in **Connected accounts**. A provider with a different email can be connected to your existing account. Connecting it does not change your GraphOps account email or move your organization memberships. ### Disconnect a provider Keep another working sign-in method before disconnecting a provider. GraphOps requires proof using another permitted method and prevents removal of your last usable sign-in method. An authenticator app or a security key used only as a second factor does not replace a primary sign-in method. Open the connected account's action menu, choose **Disconnect**, and complete the verification and confirmation. Check that the connection disappears when you return. Disconnecting a provider does not end existing GraphOps sessions; use the session controls below when you also want to end access from a device. ## Add two-factor authentication Use **Two-factor authentication** to add another check to your sign-in: * Choose **Set up authenticator** to enroll an authenticator app and confirm its code. Once enrolled, use **Manage authenticator** to manage it. * Choose **Add security key** to enroll a security key, or **Manage security keys** to manage existing keys. Follow the hosted sign-in prompts and check the configured status when you return to **Two-factor authentication**. Adding or removing an authenticator app shows there as soon as you return. If GraphOps says the change was saved but is not ready, or could not be confirmed, open **Two-factor authentication** again to check its status before changing it again. Two-factor authentication requires a second factor every time you sign in with your password or a passkey. After a passkey sign-in, use your authenticator app: a security key cannot be checked in the same sign-in as a passkey, so sign in with your password or a connected provider when a security key is your only second factor. Your sign-in policy determines which methods are available and whether a second factor is required; it can prevent removal of a required factor. ## Remove a passkey Before removing a passkey, make sure you can sign in with a password or a connected provider. Passkey removal requires fresh proof with one of those methods; another passkey alone is not sufficient. In **Passkeys**, open the named key's action menu, choose **Remove passkey**, and complete the verification and confirmation. Check that the key disappears from the list. Enroll a replacement before removing a key you still depend on. ## Stay signed in After you sign in, GraphOps remembers your sign-in on that browser. When your platform session needs renewing, choosing **Sign in** continues without asking for your credentials again until that sign-in expires. Signing out, or ending the session below, ends it on that browser. Changing your sign-in methods or two-factor authentication still asks you to confirm it's you. ## End a device session Review **Sessions** for devices you no longer use or do not recognize. Compare the device, IP address, and update time before selecting a session. The IP address is recorded when that session starts. It can be shared by people using the same network or VPN, and it does not change when you view the list from another connection. Older sessions can retain an address recorded before an IP collection correction. Completing a sign-in check from **Account** signs you in with a new session on that browser and ends the session it replaced, so repeated checks do not add sessions. To end a session, open its action menu, choose **End session**, and confirm. Check that the session is removed. Ending your current session signs you out, so sign in again to continue. Sessions labeled **CLI** belong to the GraphOps CLI. Ending one signs the CLI out, and it must sign in again; see [sign in to the CLI](/platform/cli-sign-in#end-a-cli-session). Ending a device session also ends existing support account access across your signed-in tabs. Reload the platform from a session you kept to reconnect support. If a removal was interrupted and **Check removal** appears, use it to confirm the outcome before attempting another removal. If the result remains unresolved, contact [support](./support-and-status) with the action, approximate time, and displayed error. Never send passwords, verification codes, or session credentials. ## Next steps * [Team and roles](/platform/team-and-roles): Invite teammates and choose their roles. * [API keys](/platform/api-keys): Protect the keys your applications use. * [Support and status](/platform/support-and-status): Get help with an account or access issue. --- # API keys Source: https://docs.graphops.xyz/platform/api-keys Connect your application to a GraphOps service with an API key. Use separate keys for each product, environment, and application so you can see which workload uses credits and replace one credential without touching the others. [Open API keys](https://platform.graphops.xyz/organizations/current/api-keys) ## Create a key You need the owner or developer role. 1. Open [API keys](https://platform.graphops.xyz/organizations/current/api-keys) in the organization that should pay for the usage. 2. Choose the product, such as RPC, Subgraphs, or Substreams. 3. Give the key a label that names its workload and environment, such as `production-indexer`. 4. Copy the key into your secret manager when it is shown. You cannot retrieve the same key again. To connect right away, choose **Get query endpoint** after saving the key. You get the endpoint, authentication header, and example code for the new key without pasting it again. The example code reads the key from the `GRAPHOPS_API_KEY` environment variable, and its prerequisites include the command that sets it. If creation reports **Data services unavailable**, select **Retry** to load the products you can choose. Your label stays in the dialog while you refresh. ## Use a saved key When setting up an endpoint, paste the key from your secret manager. If the dialog reports **API keys unavailable**, select **Retry** to load your organization's keys before continuing. Your pasted key stays in the dialog during refresh; closing the dialog clears it. Each service's authentication page shows the header or URL format it expects. ## Understand key access A key works for one product in one organization. It does not grant access to other products or other organizations. | Property | Behavior | | ------------ | ----------------------------------------------------------------------------------------------------------- | | Organization | Usage is billed to and limited by the organization that created the key. | | Product | The key works only for the product chosen at creation. | | Label | Identifies the environment, owner, or workload in **API keys** and **Usage**. | | Visibility | The plaintext key is shown once, at creation. GraphOps cannot show it again. | | Expiry | Keys have no expiration date by default. Plan rotation yourself. | | Active keys | Each organization can have up to 50 active keys. Revoke unused keys, or contact support to raise the limit. | ## Rotate and revoke _The plaintext key is shown once at creation, so store it before closing the dialog. To replace a key without a gap, create a second key, confirm traffic, then revoke the old one. Rotate replaces a key immediately; revocation is permanent._ 1. Create: One product, one organization. 2. Store: Shown once: save it in a secret manager. 3. Use: Deploy to one workload. 4. Rotate: Replaces the key at once; the old one stops working. 5. Revoke: Permanent. Choose the replacement path by whether the old key must keep working during the change: * **Planned replacement:** create a second key for the same product, deploy it to the workload, confirm traffic succeeds, then revoke the old key. The workload has no gap in access. * **Exposed key:** choose **Rotate API key**. The old key stops working immediately, and a new key for the same product and label is shown once. Save it, confirm you saved it, then deploy it to the affected applications. To get its endpoint and example code, choose **Get query endpoint**. Revocation is permanent: any request that still uses a revoked key fails. [Manage API keys](https://platform.graphops.xyz/organizations/current/api-keys) ## Find unused keys Check **Last used** in **API keys**. It shows the most recent UTC date with settled usage for each key, such as **yesterday** or **3 days ago** within the past week, or **Never**. The date updates when usage settles, so recent requests can take some time to appear. A rotated replacement starts at **Never**, and the replaced key keeps its date. Revoke keys that no longer receive traffic instead of leaving them available. ## Permissions Owners and developers can create, rotate, and revoke API keys. Billing admins and viewers cannot see or manage API keys. See [Team and roles](/platform/team-and-roles) for every role. ## Security checklist * Store keys in a secret manager or server-side runtime configuration. * Keep keys out of source code, issue trackers, support messages, and client-side bundles. * Use separate keys for development, staging, production, and each product. * Use labels that make ownership obvious during incident response. * Rotate a key before changing ownership or after a suspected exposure. ## Next steps * [RPC authentication](/rpc/authentication): Send the key in a header or the endpoint path. * [Subgraphs authentication](/subgraphs/authentication): Add the key to GraphQL requests. * [Substreams authentication](/substreams/authentication): Pass the key to the CLI or your client. * [Usage and limits](/platform/usage-and-limits): See which keys drive your credit usage. --- # Billing and credits Source: https://docs.graphops.xyz/platform/billing-and-credits Keep your services funded without surprises. Choose how many credits your organization needs each month, buy extra credits when a workload needs them, and turn on automatic top-ups so a busy period does not pause your services. Every service in your organization draws from one credit balance. [Manage billing](https://platform.graphops.xyz/organizations/current/billing) ## Understand credits Credits measure service usage. Each service charges for different work; use the current [RPC](/rpc/usage-and-limits), [Subgraphs](/subgraphs/usage-and-limits), and [Substreams](/substreams/usage-and-limits) rates to estimate your workload. Credits belong to your organization, not to an API key or a teammate. Creating another key does not create a separate balance. _Plan credits, top-ups, automatic top-ups and grants add credits to one organization balance, and every service draws from it, plan credits first. When the balance runs low, alerts fire and automatic top-ups can add credits; at zero or below services pause, and topping up above zero restores them automatically._ - Monthly plan: Plan credits each paid month; used first, roll over one month. - Top-ups: One-time purchases that never expire. - Automatic top-ups: Added when the balance falls below your threshold. - Grants and adjustments: New-organization grant and support credits. - Funded: Services run and draw usage. - Low balance: Alerts fire at your thresholds; automatic top-ups add credits when on. - Zero or below: Requests pause; with a plan, running services stop after a grace period. - Topped up: Service restores automatically above zero. Your balance can hold credits from several sources, and they last for different periods: | Credits | Where they come from | How long they last | | --------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Plan credits | Each paid month of your monthly plan, and each increase to your plan | Until the end of the billing period after the one they were added for | | Top-up credits | Top-ups you buy and automatic top-ups | They never expire | | Granted credits | The one-time grant for a new organization, and credits added by GraphOps support | They never expire | GraphOps uses plan credits first, oldest first, and then credits that never expire. Unused plan credits therefore roll over for one billing period. For example, if your plan renews on the 1st, plan credits added on November 1 can be used until January 1. Credits returned to your balance by a usage correction never expire. Billing shows your **Plan credits** beside the balance, including how many expire at the end of the current billing period. If your plan ends, the plan credits from your final billing period stay usable for one calendar month after it ends. If your balance or plan is unavailable in Billing, contact [support](./support-and-status) with the organization and displayed error. An unavailable balance does not mean you have zero credits, and refreshing the page does not create a new billing account. ## Choose your monthly credits Your organization can have one monthly plan, sized in steps of 100M credits. Each additional 100M costs less than the one before, down to a floor, so the more credits you commit to, the less you pay for every credit. Two numbers describe any size: * The monthly price is what your plan renews for each month, before tax. * The price per million credits is your average price for one million credits: your monthly price divided by your plan's credits. While your plan is active, it is also what top-ups and automatic top-ups cost. The list price is the price of the first 100M, applied to every credit. Your volume discount is the difference between that and your monthly price. The first 100M credits cost $39 a month. Each additional 100M costs $1 less than the one before, down to $20. Current monthly plan prices. Prices are shown before applicable taxes; Stripe Checkout shows applicable taxes and the total before payment. Prices as of Nov 12, 2026, 3:39 AM UTC. | Monthly credits | Monthly price | Price per million credits | | --- | --- | --- | | 100M | $39 | $0.390 | | 500M | $185 | $0.370 | | 1B | $345 | $0.345 | | 2.5B | $690 | $0.276 | | 5B | $1,190 | $0.238 | | 10B | $2,190 | $0.219 | Every size from 100M to 10B credits, in steps of 100M, is available. Billing shows the exact price for any size before you subscribe. To start a plan: 1. Select the intended organization. An **Owner** or **Billing admin** opens **Billing → Plan**. 2. Choose a size with the slider, or adjust it 100M at a time with the \**−*\* and \*\*+\*\* buttons. The page starts at the smallest plan, and the labels under the slider show the volume discount at each size. The price reads like a receipt: your credits at the list price, your volume discount, and what you pay each month, with your price per million credits and what the next 100M would add. When your last 30 days used more credits than your current plan adds, the page suggests a size that covers them. 3. Continue to Stripe Checkout. Checkout shows applicable taxes and the total before you pay. 4. Return to Billing after payment. Your plan credits arrive when the payment is confirmed; refresh Billing to check your plan and balance. If a successful payment is still missing after refreshing, contact [support](./support-and-status) with the checkout time and receipt. Plans bill monthly in US dollars, and you pay by card or Link. Free trials are not available. A promotion code for a new plan applies to its first invoice only and does not change your price per million. Top-ups do not accept promotion codes. Each organization has its own plan and price per million. Organizations do not combine their usage to reach a lower price. ### Organization limits Credits are the main constraint on your usage. Two limits apply to every organization, whatever its plan size: | Limit | Default | | ----------------- | ------------------- | | Active API keys | 50 per organization | | Managed Subgraphs | 10 per organization | Contact [support](./support-and-status) to raise a limit for your organization. Request rates and other service limits are on each service's usage page. ### Managed Subgraphs need a monthly plan To create a Managed Subgraph, your organization needs a monthly plan and a positive balance. A plan that is set to cancel still counts until it ends; a plan whose payment needs attention does not. After a plan ends, existing Managed Subgraphs keep running while your balance stays positive, but they stop without a grace period when credits run out. See [Keep services funded](#keep-services-funded). ## Buy extra credits Buy a top-up when a workload needs more credits than your plan adds, or to use GraphOps without a monthly plan. A top-up is a one-time purchase of credits in steps of 10M. It does not renew, and top-up credits never expire. Current top-up prices. Prices are shown before applicable taxes. Prices as of Nov 12, 2026, 3:39 AM UTC. | Top-up price | Price per million credits | Purchase limit | | --- | --- | --- | | Plan price | Your plan's price per million | Up to your plan size each billing period | | Pay-as-you-go price | $0.500 | Up to 2B every 30 days | Each purchase buys credits in steps of 10M, up to 10B at a time. Which price you pay depends on your plan: * **Plan price:** with an active monthly plan, top-ups cost your plan's price per million, up to your plan size in top-ups each billing period. This is your plan-price allowance: on a 2.5B plan, you can buy up to 2.5B credits of top-ups at your price per million each billing period. * **Pay-as-you-go price:** applies without a monthly plan, while a plan is set to cancel, while a plan payment needs attention, and beyond your plan-price allowance. It is higher than the price of the first 100M on a plan, so a plan is always the cheaper way to buy recurring credits. Pay-as-you-go purchases are limited per organization over a rolling 30 days. To buy a top-up: 1. An **Owner** or **Billing admin** opens **Billing → Top-ups**. 2. Choose how many credits to buy, in steps of 10M. The page shows the price per million credits that applies and where it comes from, how much of your plan-price allowance remains this billing period, and the total before tax. 3. Complete Stripe Checkout. The credits arrive when the payment is confirmed, and the invoice appears in your invoice history. When your plan-price allowance is used up, the page shows the pay-as-you-go price before you continue. To keep adding credits at plan prices, [increase your monthly credits](#change-monthly-credits) instead: you pay only the difference now and receive the added credits immediately. A top-up does not settle an unpaid plan invoice. Resolve a payment warning from Billing first; see [Manage invoices and payment details](#manage-invoices-and-payment-details). If a payment to GraphOps is disputed or your card issuer reports it as fraudulent, GraphOps puts new purchases on hold: top-ups, plan increases and automatic top-ups are unavailable, and automatic top-ups turn off. Your plan and existing credits are unaffected. Contact [support](./support-and-status) to resolve the hold. ## Automatic top-ups Keep your services running through a traffic spike without watching your balance. With automatic top-ups on, GraphOps buys credits for you whenever your balance falls below a threshold you choose, up to a monthly spending limit you set. Automatic top-ups need an active monthly plan. An **Owner** or **Billing admin** sets them up in **Billing → Top-ups**. ### Choose your settings The settings read as one sentence: when my balance falls below a threshold, add a number of credits, up to a monthly spending limit. | Setting | Allowed values | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Top-up threshold | A multiple of 10M credits, from 10M up to your monthly plan credits. | | Credits per top-up | Steps of 10M, from 10M up to your monthly plan credits. | | Monthly spending limit | Before tax. At least the cost of one top-up, and at most your monthly plan price during your first three paid months, then three times your monthly plan price. Contact support to raise it further. | Billing suggests a threshold from your recent peak usage and a limit equal to your monthly plan price. Keep the threshold well above zero: usage is recorded after it happens and a payment takes a little time, so a higher threshold lets new credits arrive while your services are still funded. Each automatic top-up costs its credits at your price per million, plus tax. The price follows your plan size: if a renewal changes your price per million, later automatic top-ups use the new price and GraphOps emails you before they do. ### Turn on automatic top-ups 1. Enter your settings. The panel shows the cost of each top-up and the most you could pay this billing period, including your plan. 2. Read the authorization statement. It names the threshold, the amount, your current price per million, the spending limit, and how to turn automatic top-ups off. 3. Continue to Stripe Checkout and authorize a card or Link for automatic charges. GraphOps uses this payment method only for automatic top-ups; your plan keeps its own default payment method. 4. Return to Billing and check that automatic top-ups are **On**. If your balance is already below the threshold, the first automatic top-up runs as soon as you finish, and the panel tells you before you authorize. Changing the threshold or the credits per top-up, or raising the limit, asks you to accept the updated statement. Lowering the limit does not. To use a different card, authorize again. A top-up that hasn't been charged yet always follows your current settings, so a change you save just before the charge applies to it. ### What happens when your balance falls below the threshold * GraphOps charges one automatic top-up to the authorized card. Your card statement shows `GRAPHOPS TOPUP`. * If your balance is far below the threshold, for example negative after a burst, the top-up adds enough 10M steps to bring it back to at least the threshold. * One automatic top-up runs at a time. If your balance is still below the threshold after it is paid, another follows. * A top-up that would take this billing period's automatic top-ups over your spending limit is reduced to the largest number of 10M steps that fits. When no 10M step fits, automatic top-ups stop until your next billing period. Owners and billing admins receive a warning when automatic top-ups reach 80% of the limit, and a notice when the limit stops them. * Each successful automatic top-up emails a receipt to the billing email, owners, and billing admins, with the amount, the card, and this period's spending against the limit. You can turn these receipts off in **Billing → Settings**. * **Top-up activity** lists every manual and automatic top-up with its credits, amount, tax, status, and invoice. You can export it as CSV. If automatic top-ups run very often, Billing suggests more credits per top-up or a larger plan. ### When an automatic payment fails * If your bank declines a payment temporarily, GraphOps retries once. Other declines fail right away. * If your bank asks you to confirm a payment, GraphOps emails you. Confirm it within 24 hours; see [Complete a pending payment](#complete-a-pending-payment). After 24 hours, the payment is canceled and treated as failed. * After a failed payment, automatic top-ups pause. The billing email, owners and billing admins receive a notice with the reason and your current balance. Low-credit alerts and the usual behavior at zero credits continue. * Removing the authorized card, or a card update that changes its brand, also pauses automatic top-ups. To resume, authorize a card again in **Billing → Top-ups**. This turns automatic top-ups back on in one step. ### Turn off automatic top-ups Turn automatic top-ups off in **Billing → Top-ups** at any time. A top-up that has not started charging is canceled; one that is already being charged finishes. Automatic top-ups also turn off, with a notice to owners and billing admins, when: * your plan is set to cancel or ends; * a payment dispute or fraud warning places a hold on purchases. Billing shows **Held by GraphOps** until GraphOps support reviews it. Turning them back on afterwards needs a new authorization. They also do not run while a plan payment needs attention or while GraphOps has disabled the organization. When a renewal lowers your plan size, any setting above the new maximum is lowered to fit, and you receive a notice. GraphOps never raises your settings automatically. ## Change monthly credits Change your plan size whenever your usage changes. Open **Billing → Plan**, choose the new size, and review the change before confirming it. | Change | When it takes effect | What you pay | Credits | | ----------------------------------------------------- | -------------------- | ------------------------------------------------------- | ------------------------------ | | Increase above the size you have paid for this period | Immediately | The difference between the two monthly prices, plus tax | The added credits, immediately | | Any size up to the size you have paid for | At your next renewal | Nothing today, and no refund | The new size at renewal | An increase is charged to your plan's default card right away. For example, if you have paid for 1B this period and increase to 2.5B, you pay the 2.5B monthly price minus the 1B monthly price, plus tax, and receive 1.5B credits now. From your next renewal you pay the 2.5B monthly price. Your billing date does not change. Large increases ask you to confirm the amount a second time. A decrease leaves your current size, credits, and price per million in place until renewal. Billing shows the new size and the date it takes effect; choose **Cancel scheduled change** before then to keep your current size. At renewal, your new price per million applies to top-ups and automatic top-ups too. GraphOps does not prorate plan changes or refund part of a period. You cannot change your plan while it is set to cancel (choose **Keep my plan** first), while a plan payment needs attention, while an earlier increase is waiting for payment, or for the few seconds while a paid increase is applied. If you increase your plan by mistake, contact [support](./support-and-status) within 24 hours. Support can refund an increase when less than 5% of its added credits have been used. The refund removes the added credits and returns your plan to its previous size. ### Complete a pending payment Billing shows a notice when a plan increase or an automatic top-up is waiting for you to pay or to confirm with your bank. This happens when there is no card on file, or when your bank declines the payment or asks you to confirm it. * Choose **Continue** to open the secure payment page, then pay with another card or confirm with your bank. The credits arrive when the payment succeeds. * Choose **Cancel** to abandon a plan increase. Nothing changes, and you are not charged. * A plan increase that is not paid within 24 hours, or by the end of your billing period if that comes first, is canceled automatically. You can then request the change again. * An automatic top-up that is not confirmed within 24 hours is canceled, and automatic top-ups pause. If you leave Stripe Checkout before paying for a new plan or a top-up, start again from Billing. An unpaid top-up Checkout expires after a short time and releases any plan-price allowance it held. If a purchase reports an error, refresh Billing and check for an updated plan or a pending payment before trying again. If the state remains unclear, contact support with the displayed error and approximate time. ## Cancel your plan Before you cancel, check what changes: * Your plan stays active until the end of the current billing period, subject to available credits. There is no refund for the rest of the period. * Automatic top-ups turn off immediately, and owners and billing admins receive a notice. * Until the plan ends, top-ups cost the pay-as-you-go price, and you cannot change your monthly credits. * When the plan ends, plan credits carried over from earlier periods expire. Plan credits from your final billing period stay usable for one more calendar month. Top-up credits never expire. * After the plan ends, you cannot create Managed Subgraphs. Existing ones keep running while your balance is positive, then stop without a grace period when credits run out. To cancel, choose **Cancel plan** in Billing, review what changes, then confirm the end date in the secure billing portal. Billing shows your plan as **Ending** until the end date. Choose **Keep my plan** before then if you want it to renew. After the plan ends, Billing shows **No monthly plan**. You can keep using your remaining credits, buy pay-as-you-go top-ups, and start a new plan at any time. ## Understand your invoices Every charge produces an invoice in your invoice history. Prices are shown before tax, and tax is calculated from your billing details. | Invoice | What it charges | When | | ------------------ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------- | | First plan invoice | The monthly price of the size you choose | When you subscribe | | Renewal | The monthly price of your renewal size | On each billing date | | Plan increase | The difference between the monthly prices of the new size and the size you have paid for this period | When you increase | | Top-up | Its credits at the price per million shown when you bought it | When you complete Checkout | | Automatic top-up | Its credits at your price per million | When your balance falls below your threshold | Here is how one month could look on a plan that renews on the 1st. Look up the monthly prices in [Choose your monthly credits](#choose-your-monthly-credits). Each invoice adds tax. 1. **November 1:** You subscribe to 500M. The first invoice charges the 500M monthly price, and 500M plan credits arrive. 2. **November 10:** Usage grows, so you increase to 1B. An increase invoice charges the 1B monthly price minus the 500M monthly price, and 500M plan credits arrive immediately. 3. **November 20:** You buy a 300M top-up. Its invoice charges 300 times your 1B price per million. You can buy up to 700M more at that price before December 1. 4. **November 25:** Your balance falls below your automatic top-up threshold, and an automatic top-up adds 300M credits at your price per million. 5. **December 1:** The renewal invoice charges the 1B monthly price, and 1B plan credits arrive. Unused November plan credits stay usable until January 1. ## Get notified before credits run low Set low-credit alerts to help avoid interrupted service. In **Billing → Settings**, an **Owner** or **Billing admin** can set the billing email and configure alerts: 1. Enable low-credit alerts. 2. Add up to five different remaining-credit thresholds. Each is an absolute, non-negative whole number of credits. Suggested thresholds are based on your monthly plan credits; they are editable credit amounts, not percentages that track future plan changes. 3. In **Recipients**, choose who receives alerts: the billing email, active members in selected roles, or both. Enabled alerts need at least one recipient; if none is selected when you save, the requirement appears under **Recipients**. 4. Save the settings and check that the saved thresholds and recipients match your choices. The billing email is selected by default. Deselect it when alerts should reach only members in the selected roles; their addresses stay hidden from one another. Each address receives one email, even if the billing email also belongs to a selected member. If your organization has no billing email, add one under **Billing contact** so a selected billing email receives alerts. Until then, alerts go only to the selected roles, so choose at least one role. Each threshold alerts once when the balance crosses down to or below it. After you replenish credits above that threshold, it can alert again on a future drop. When automatic top-ups are on, alerts below your automatic top-up threshold rarely fire, because a top-up usually restores the balance first. ## Keep services funded When your available credits reach zero or below, your organization's services are paused unless support has arranged an exception. Requests to your services stop being served right away. With a monthly plan, services that run continuously, such as Managed Subgraphs, keep running and accruing their usual charges until the end of a grace period, then stop. Without a monthly plan, or while a plan payment needs attention, they stop right away. While your balance is zero or below, you cannot start or change services. _Requests pause as soon as the balance reaches zero or below. With a monthly plan, running services such as Managed Subgraphs keep running and charging until the grace period ends, then stop; without a plan they stop right away. Topping up above zero restores service automatically unless GraphOps support applied the pause._ | Balance | Requests | Running services | Start or change services | | --- | --- | --- | --- | | Above zero | Served | Running | Allowed | | Zero or below | Paused right away | Keep running with a plan | Blocked | | Grace period ends | Paused | Stopped | Blocked | | Topped up above zero | Restored automatically | Restoration requested | Allowed | Turn on [automatic top-ups](#automatic-top-ups) so credits are added before your balance runs out. Before a large backfill or expected traffic increase, also check your balance, review recent [usage](./usage-and-limits), and add credits if needed. Every member of a paused organization sees a notice at the top of each organization page. An **Owner** or **Billing admin** can top up credits from it; other members are asked to contact one. Billing shows the same pause in detail, including when the grace period ends. Top up enough credits to bring your available balance above zero. Service is restored automatically; you do not need to resume anything manually. A pause applied by GraphOps support is not lifted by topping up. Contact support instead. If your organization is also out of credits, running services still stop when the grace period ends unless you top up. Owners and billing admins receive mandatory service notices when services are paused and when running services stop, together with the configured billing contact. Turning off optional low-credit alerts or changing their recipients does not affect these notices. Use the grace period end shown in Billing to plan recovery even if you have not received an email. After a top-up or credit allocation, refresh Billing to check the available balance and service status. If the balance is positive but access or indexing remains unavailable, contact support. ### Managed Subgraphs during a pause With a monthly plan, both Current and Next versions keep indexing during the grace period, with their usual indexing charges, even while requests are paused. Without a monthly plan, or while a plan payment needs attention, there is no grace period: indexing stops as soon as your balance reaches zero. You cannot start new indexing, stage an upgrade, or restore indexing while your balance is zero or below unless support has arranged an exception. When the grace period ends, or right away without one, GraphOps requests that your organization's affected Current and Next versions stop indexing. Another organization funding the same deployment does not keep your versions indexing. Indexing charges can continue until disconnection from the indexing service is confirmed. Your Managed Subgraphs, Endpoint Aliases, and version history remain available for recovery. After you top up, GraphOps automatically cancels a credit-related stop request or requests restoration of indexing stopped because of exhausted credits. An operator pause or a resource scheduled for deletion is not restored automatically. If indexing capacity or stored data has been released, restoration may require indexing again. Check [indexing health](../subgraphs/guides/monitor-sync-health) before relying on the recovered data. ## Manage invoices and payment details Open **Billing → Settings** and use **Payment and tax details** to choose the task: [Billing settings](https://platform.graphops.xyz/organizations/current/billing/settings) * **Invoices & receipts:** open invoice history to download plan, increase, top-up, and automatic top-up invoices, or pay an outstanding invoice. You can also open this from the Billing sidebar. * **Payment methods:** open payment management and use its Wallet section to manage saved methods. This action is also available in the Billing sidebar. * **Billing address & tax details:** update your legal name, billing address, and tax IDs for future invoices. The **Billing contact** below these actions controls your organization's billing email, which also receives automatic top-up receipts and, when it is selected as a recipient, low-credit alerts. For a payment warning, use **Resolve payment issue** on the warning, then return to Billing and refresh to check the result. The card you authorize for automatic top-ups is separate from your plan's default payment method. Changing your default payment method does not change it; authorize a card again in **Billing → Top-ups** instead. Removing the authorized card from your saved methods pauses automatic top-ups. Stripe Checkout collects the billing information needed for your purchase, including a business tax ID when applicable, and calculates applicable taxes before payment. ## Next steps * [Usage and limits](/platform/usage-and-limits): Find which workloads use your credits. * [Team and roles](/platform/team-and-roles): Give a teammate the billing admin role. * [Support and status](/platform/support-and-status): Get help with a payment or balance. --- # Sign in to the CLI Source: https://docs.graphops.xyz/platform/cli-sign-in Use the GraphOps CLI with your own account without copying passwords or tokens into a terminal. The CLI shows a short code; you approve it in a browser where you can use your usual sign-in methods, and the CLI finishes signing in on its own. ## Sign in 1. Run `graphops auth login`. The CLI shows a sign-in address and a code. 2. Open the address in a browser and enter the code exactly as the CLI shows it. 3. Sign in with your password or a passkey, and complete two-factor authentication if your sign-in requires it. You can also choose Google or GitHub when that provider is already connected to your GraphOps account. 4. Check that the code on the approval page matches the code in your terminal and that the page shows the account the CLI should use, then choose **Allow**. To use a different account, choose **Use another account** and sign in again. The CLI finishes signing in after you allow the request, and the browser tells you to return to your terminal. If the code expires before you approve it, run `graphops auth login` again to get a new code. Approving always asks you to sign in, even when the browser is already signed in to GraphOps. The approval signs in only the CLI; it does not sign that browser in to the platform. Your account's email address must be verified before you can approve the CLI. > **Approve only your own codes** > > Approving a code gives the CLI that requested it access to your account. Enter only codes that you > started from your own terminal, and choose **Deny** if you did not run `graphops auth login` or > the codes do not match. ## What you approve Allowing the request lets the GraphOps CLI: * read your name and email address; * send requests as your account, with the role you have in each of your organizations; * stay signed in without asking you again, until its session ends. The CLI does not receive your password, passkey, or two-factor codes. Approval never creates an account or connects a provider: to approve with Google or GitHub, [connect the provider](/platform/account-and-security#connect-google-or-github) from **Account** first. If you choose a Google or GitHub account that isn't connected to GraphOps, the browser stops and the request stays unapproved; connect the provider, then run `graphops auth login` again. ## How long the CLI stays signed in Each approval creates a CLI session that lasts for a limited period. When it ends, the CLI can no longer send requests; run `graphops auth login` to sign in again. To sign the CLI out before then, [end its session](#end-a-cli-session): the CLI is signed out immediately. ## End a CLI session End a CLI session when you no longer use the CLI on that computer, or when you do not recognize it. In **Account**, find the session under **Sessions**. CLI sessions show **GraphOps CLI** with the **CLI** label. Open the session's action menu, choose **End session**, and confirm. The CLI's next request is refused, including after it refreshes its sign-in, and it must sign in again. Ending a CLI session does not sign out your browser sessions. See [end a device session](/platform/account-and-security#end-a-device-session) for the other session controls. ## Next steps * [Account](/platform/account-and-security): Manage the sign-in methods you use to approve the CLI. * [Support and status](/platform/support-and-status): Get help with an account or access issue. --- # Frequently asked questions Source: https://docs.graphops.xyz/platform/faq Find a short answer to common questions about credits, billing, usage, API keys, and team access. Each answer links to the page that owns the full guidance. ## Credits and billing ### What is a credit? A credit is the unit GraphOps uses to meter service usage. Credits belong to the organization, and each service maps work to credits differently, so check the service's usage page before estimating production cost. ### How is the monthly plan priced? You choose how many credits your organization needs each month, in steps of 100M. Each additional 100M costs less than the one before, down to a floor, so your price per million credits falls as your plan grows. See [current plan prices](./billing-and-credits#choose-your-monthly-credits). ### Do credits renew? Plan credits are added each billing period after that invoice is paid; top-ups do not renew. See [credit sources](./billing-and-credits#understand-credits). ### Do my credits expire? Plan credits last until the end of the billing period after the one they were added for, so unused plan credits roll over for one billing period. Top-up credits, automatic top-up credits, and credits granted by GraphOps never expire. See [credit lifetimes](./billing-and-credits#understand-credits). ### Are top-ups subscriptions? No. A top-up is a one-time purchase of credits in steps of 10M, and its credits never expire. Automatic top-ups are also one-time purchases, made for you when your balance falls below the threshold you set. ### Why does a top-up cost more than adding 100M credits to my plan? A top-up costs your plan's average price per million. Adding 100M credits to your plan costs that step's own price, which is lower because each additional 100M costs less than the one before; in exchange, your monthly price rises from your next renewal. Without an active plan, or beyond your plan-price allowance, top-ups cost the higher pay-as-you-go price. See [Buy extra credits](./billing-and-credits#buy-extra-credits). ### Why was I charged while I still had credits? Automatic top-ups charge when your balance falls below your threshold, not when it reaches zero, so your services stay funded. A plan increase charges the difference as soon as you confirm it, and your plan renews each month whatever your balance. Check **Top-up activity** and your invoice history in Billing to see each charge. See [Automatic top-ups](./billing-and-credits#automatic-top-ups). ### What happens after checkout? Checkout shows the price, taxes, billing details, and total before you pay. After payment succeeds, GraphOps updates the organization's plan and credits; refresh Billing to check them. ### How do I complete a payment that needs my action? Billing shows a notice when a plan increase or an automatic top-up needs you to pay or to confirm with your bank. Choose **Continue** on the notice within 24 hours. See [Complete a pending payment](./billing-and-credits#complete-a-pending-payment). ### Do I get a refund when I lower my plan? No. A lower plan size takes effect at your next renewal, so you keep the size, credits, and price per million you have paid for until then. Increases are charged immediately and are not prorated. See [how plan changes work](./billing-and-credits#change-monthly-credits). ### What happens to my credits and Managed Subgraphs after I cancel? Your plan stays active until the end of the billing period, and automatic top-ups turn off right away. After it ends, your final period's plan credits stay usable for one calendar month, and top-up credits never expire. Existing Managed Subgraphs keep running while your balance is positive, then stop without a grace period. You cannot create new ones without a plan. See [Cancel your plan](./billing-and-credits#cancel-your-plan). ### What happens when credits run out? Requests pause as soon as the available balance reaches zero or below. With a monthly plan, running services such as Managed Subgraphs stop when the grace period ends; without one, they stop right away. Topping up above zero restores service automatically, and automatic top-ups help you avoid the pause; see [keep services funded](./billing-and-credits#keep-services-funded). ### Which currency and taxes apply? Prices are in US dollars and shown before tax. Stripe Checkout and each invoice calculate the applicable tax from your billing details, including a business tax ID when you provide one. ### Can I pay by invoice or bank transfer? Not with self-serve billing. Plans and top-ups are paid by card or Link. To discuss invoiced payment or a larger commitment, contact [GraphOps support](./support-and-status). ### Do organizations share volume? No. Each organization has its own plan, price per million, and balance, so usage in one organization does not lower the price in another. ### Where do I manage invoices, taxes, and payment methods? Open **Billing → Settings** and choose the task under **Payment and tax details**. [Billing settings](https://platform.graphops.xyz/organizations/current/billing/settings) See [payment details](./billing-and-credits#manage-invoices-and-payment-details). ## Usage and limits ### Where do I confirm my active limits? Each organization can have up to 50 active API keys and 10 Managed Subgraphs by default, whatever its plan size; support can raise them. Your plan and credit balance are in **Billing**, and current consumption is in **Usage**. Each service's usage page explains its request limits; contact support to confirm organization-specific values before production traffic depends on them. See [organization limits](./billing-and-credits#organization-limits). ### Why is usage not visible immediately? Usage can take a few minutes to appear after requests, streams, or jobs complete. If activity is still missing after a few minutes, check the UTC time range and filters, then contact support with your organization, product, approximate UTC time, and request ID. ### Which time zone does usage use? Hourly and daily totals follow UTC boundaries. Hover over a time to see its full date, time, and time zone, and include both the UTC range and your local time zone when asking about a discrepancy. ## API keys ### Can one API key access multiple products? No. A key works for one product in one organization, so use separate keys for each product and environment. ### Is an API key shown again after creation? No. The plaintext key is shown once; store it in a secret manager or server-side runtime configuration before closing the dialog. ### Which roles can manage API keys? Owners and developers. Billing admins and viewers cannot see or manage API keys. ## Team and account ### Which roles can manage billing? Owners and billing admins. Developers and viewers cannot. ### Is there an Admin role? No. Use the owner role for team administration and the billing admin role for payment responsibilities; custom roles are not available. ### How long do invitations last? Pending invitations expire after 7 days. Owners can revoke a pending invitation before it is accepted, or resend a pending or expired invitation to give the recipient another 7 days. ### How do I change my sign-in methods or secure a lost device? Open **Account** from the sidebar or your account menu to manage passwords, passkeys, connected accounts, two-factor authentication, and device sessions. See [Secure your account](./account-and-security). [Account](https://platform.graphops.xyz/account) ## Services and support ### How do I choose a service? Use RPC for node calls, transactions, logs, and chain-state reads; Subgraphs for GraphQL entity reads; and Substreams for ordered streams, backfills, and sink pipelines. See [Choose a data service](/choose-a-service). ### What should I know before updating an Endpoint Alias? Changing its target can change the schema served to every client that uses it. Test schema compatibility, keep the previous Deployment CID available, and plan a rollback before retargeting a production Endpoint Alias. ### Where do I get support or service-status information? [Open the support chat](https://platform.graphops.xyz/organizations/current/overview?support=open) in the platform for organization-specific questions and current incident information. See [Support and status](./support-and-status) for what to include. ## Next steps * [Billing and credits](/platform/billing-and-credits): Choose your monthly credits, add credits, and set alerts. * [Usage and limits](/platform/usage-and-limits): Find which workloads use your credits. * [Support and status](/platform/support-and-status): Get help with anything this page does not answer. --- # Overview Source: https://docs.graphops.xyz/platform Manage the team, credentials, and budget behind your GraphOps integrations. An organization brings your services together under one credit balance and lets you give each teammate the access their work requires. New to GraphOps? [Start building](/get-started) takes you from an account to your first request. _Your account holds your personal sign-in settings and works in every organization you join. Each organization keeps its own members, API keys, credit balance and usage; none of them carry over to another organization._ - Your account: Sign-in, two-factor, sessions. - Per organization, Members and roles: Who can do what. - Per organization, API keys: One product each. - Per organization, Credit balance: Plan credits and top-ups. - Per organization, Usage: By product, key, resource. ## Manage your organization - [Connect your applications securely](/platform/api-keys): Create separate keys for each service and environment, replace credentials, and revoke unused access. - [Plan your spending](/platform/billing-and-credits): Size your monthly credits, add credits automatically when you need them, and set low-credit alerts. - [Understand your consumption](/platform/usage-and-limits): Find which products and workloads use credits, and plan capacity before traffic grows. - [Bring your team on board](/platform/team-and-roles): Invite teammates and match their access to engineering, finance, or reporting responsibilities. - [Secure your account](/platform/account-and-security): Manage sign-in methods, add two-factor authentication, and end device sessions. ## Work across organizations You can belong to more than one organization. Each organization's API keys, members, usage, and billing are separate, so select the intended organization before you create a key, buy credits, or change team access. Open your account menu and choose **Organization** to switch organizations or create one. Your personal **Account** settings apply across them. [Open the platform](https://platform.graphops.xyz/organizations/current/overview) ## Next steps * [Start building](/get-started): Go from a new account to your first authenticated request. * [Production readiness](/production-readiness): Review credentials, limits, retries, and recovery with your team. * [Support and status](/platform/support-and-status): Get help with an access issue, billing question, or incident. --- # Support and status Source: https://docs.graphops.xyz/platform/support-and-status Get the right answer quickly by starting in the right place and sending details support can act on. Contact GraphOps support from the platform when a question involves a specific organization, plan, API key, usage window, invoice, or limit. _Start with the service's troubleshooting page for an exact error. For anything tied to your organization, open Get support in the platform; the GraphOps team picks up the same conversation, so keep follow-ups there._ 1. Docs and troubleshooting: Errors, checks, FAQ. 2. Get support: Chat in the platform. 3. GraphOps team: Same conversation. Never send keys, passwords or verification codes. ## Where to start | Need | Best place to start | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Billing, invoices, payment methods, taxes, automatic top-ups, or a pending payment | [Billing and credits](./billing-and-credits) explains purchases, payment details, and recovery actions. | | A plan increase made by mistake | Contact GraphOps support within 24 hours with the organization and the time of the increase. | | Sign-in, verification email, or account security | [Secure your account](./account-and-security) covers sign-in methods, verification, and sessions. | | Limit increase, credit exhaustion, or usage discrepancy | Contact GraphOps support with the organization and UTC time range. | | API key exposure or access incident | Rotate or revoke the key first, then contact support with the key label and approximate exposure time. | | Service errors or freshness questions | Start with the service's troubleshooting page, then contact support if the issue persists. | | General service status | Ask GraphOps support in the platform chat for current incident information. | ## Contact support Open the support chat in the platform. If you are not signed in, you sign in first and the chat opens afterwards, in the organization you are working in. [Get support](https://platform.graphops.xyz/organizations/current/overview?support=open) In the platform, you can also choose **Help → Get support** in the sidebar, or **Get support** at the bottom of any dialog. Support sees which page or dialog you asked from, so you don't need to describe where you are. * If chat is still initializing, keep the platform page open and try **Get support** again once it is ready. Repeated reloads or organization switches can temporarily limit new chat initialization; wait before trying again. * If you edit a question while waiting for an answer, keep the revised question in the same conversation so support can review it when an automatic answer is withheld. * When a previous conversation is resolved, you can start another in the same signed-in session. Organization access is checked again for the new conversation. * You can sign out even if the chat widget stops working. Sign-out revokes its access before ending your session. ## Help outside the Platform Website chat and other GraphOps public support channels can help with documentation, services, general pricing and supported networks. They cannot inspect your organization's account, credits, usage or API keys, even if you use the same email address. For account-specific help, sign in to the Platform and open support for the relevant organization. You can ask for a person in the channel where you started. ## What to include Give support enough context to identify the organization and time window without exposing secrets. A request like this one can usually be investigated without a follow-up question: A reference support-request template. Replace each value with your own, and leave out plaintext API keys, passwords, verification codes and payment details. ```text Organization: acme-production Product: RPC API key label: production-indexer Time: 2026-10-21 14:00-14:20 UTC Local time zone: Europe/London Endpoint: /v1/evm/1 Request ID: Error: 429 Too Many Requests Expected: steady 20 requests per second ``` - Name or ID - The affected service - The label, not the key - UTC range - Or Endpoint Alias, Deployment CID, package - When available - Status and safe error body - What you expected instead For billing questions, add the Stripe receipt or invoice reference. Attach a screenshot when it helps, with secrets redacted. Never send plaintext API keys, passwords, verification codes, session credentials, URLs that contain a key, or payment card details. If a key was exposed, rotate it before opening the support request. ## What support can see To answer questions about your account, support can look up the organization you have selected: its services, Endpoint Aliases, Managed Subgraphs, API key labels, billing, credits, and usage. Support sees only what your role lets you see. For example, member and invitation lists require an **Owner**. Support never sees plaintext API keys or payment details. Switching organizations, signing out, losing access, or resolving the conversation ends support's access to your earlier account data. Support can explain how to make a change; you make resource and billing changes in the platform. Support may share a generalized suggestion with the GraphOps team when documentation or a product flow is confusing. Keep credentials and payment details out of support messages. ## Share product feedback Suggest an improvement or tell us what could work better without leaving your current task. * On a documentation page, choose **Provide feedback** at the bottom of the article. * In the platform sidebar, open **Help → Provide feedback**. Write your feedback, optionally choose a type, and select **Send feedback**. For a problem, describe what happened and what you expected. For an improvement, explain what you want to achieve and why it matters. A success message confirms that your feedback was saved; an individual reply or implementation is not guaranteed. Use **Get support** for urgent issues or account help. Both forms include the complete page URL, application version when available, and browser details. The platform also includes your account and organization context; expand **Included context** to check it. Keep API keys, passwords, and personal details out of feedback. Feedback also records your original IP address when available. Feedback records, including that address, are retained indefinitely for product investigation and abuse prevention. Closing or reloading preserves your draft for that page (and organization in the platform) during the browser session when browser storage is available. If sending fails, use **Retry feedback** to submit the same response. ## Next steps * [Frequently asked questions](/platform/faq): Check quick answers before opening a conversation. * [Usage and limits](/platform/usage-and-limits): Find the time range and key behind a usage question. * [API keys](/platform/api-keys): Rotate or revoke a key after an exposure. --- # Team and roles Source: https://docs.graphops.xyz/platform/team-and-roles Give engineers access to integrations, finance teammates access to billing, and other teammates a read-only view. Roles apply within each organization, so the same person can have different responsibilities in different teams. ## Roles _Owners manage everything, including members and roles. Developers manage credentials and Subgraph resources, billing admins manage billing, and every role can read the dashboard and usage._ - Members and roles: Owner - API keys: Owner, Developer - Managed Subgraphs and Endpoint Aliases: Owner, Developer - Billing: Owner, Billing admin - Read dashboard and usage: Owner, Developer, Billing admin, Viewer | Role | Best for | | ------------- | ----------------------------------------------------------------------------------------------------------------------- | | Owner | Trusted administrators who manage team access, billing, and every resource. | | Developer | Engineers who operate integrations, API keys, Managed Subgraphs, and Endpoint Aliases. | | Billing admin | Finance or operations teammates who manage the monthly plan, top-ups, automatic top-ups, payment, invoices, and alerts. | | Viewer | Teammates who need visibility without change access. | Your role applies equally to the platform, direct API requests, and authenticated support reads. API key records are available to owners and developers. Billing details, credit history, and billing preferences are available to owners and billing admins. Only owners can see team membership and invitation lists. Every member can see usage and technical resources, plus the plan size, remaining credits, and active-key count on **Overview**. Choose the narrowest role that covers each person's work. Custom roles and a separate admin role are not available: use the owner role for team administration and the billing admin role for payment responsibilities. The person who creates an organization becomes its owner. ## Invite a teammate _An invitation stays pending for 7 days, and an owner can resend an expired one to reopen it for another 7 days. The recipient joins only by signing in with the invited email and accepting it; creating an account alone does not join the organization. Revoked and accepted links cannot be reused._ 1. Owner invites: Email and role. 2. Pending: Expires after 7 days. 3. Recipient accepts: Signed in with that email. 4. Member: Has the chosen role. - Expired: No answer within 7 days; an owner can resend it. - Revoked: An owner withdrew it. 1. As an **Owner**, open **Organization → Invitations**. 2. Invite the teammate by email and choose the least-privileged role that matches their job. 3. Tell the recipient which organization they are joining and which email you used. 4. The recipient signs in with the invited email, verifies it if needed, and accepts the invitation. Creating an account alone does not accept it. Check the email address before sending. If the role or recipient is wrong, revoke the pending invitation and send a new one; accepted and revoked invitation links cannot be reused. If an invitation expires before the recipient accepts it, open the **Archived** tab and choose **Resend** from its row menu. The same invitation becomes pending again for another 7 days, and the recipient gets a new email. Resend is unavailable when a newer pending invitation already exists for that email. ## Update or remove access Owners can change roles or remove members from **Organization → Members**. Review access before sensitive work begins and whenever someone's responsibilities change. When you remove someone who had access to API key secrets, rotate or revoke the keys they could have copied. Removing a member does not revoke the keys they created. When your own role changes while the dashboard is open, the next refresh verifies your membership and updates the pages you can use. You remain in the organization; a page outside your new role returns to **Overview**. ## Next steps * [API keys](/platform/api-keys): Give each workload its own key and rotate exposed ones. * [Billing and credits](/platform/billing-and-credits): Set up your monthly plan, automatic top-ups, payment details, and low-credit alerts. * [Secure your account](/platform/account-and-security): Protect owner and billing accounts with two-factor authentication. --- # Usage and limits Source: https://docs.graphops.xyz/platform/usage-and-limits Budget for growth and explain unexpected spending by finding the workloads that use your credits. All services in your organization share one credit balance, and usage shows which product, API key, or resource did the work. ## How usage is recorded _Each service records the work done for your organization, attributed to the API key that sent it or the resource that ran. Records settle into hourly UTC totals, which can take a few minutes; settlement draws credits from your organization's balance and updates Usage and each key's Last used date._ 1. Work: requests and streams sent with an api key; running resources, such as managed subgraph indexing. 2. Usage record: product, event type, key or resource, credits, UTC hour. 3. Can take a few minutes. 4. Results: Usage: hourly and daily UTC totals; Credit balance: credits drawn; API keys: Last used date. Because usage is attributed to the API key that sent the work, a separate key per workload is what lets you tell workloads apart later. ## Find what is driving usage 1. Open [Usage](https://platform.graphops.xyz/organizations/current/usage) and choose the time range you want to investigate. 2. Compare daily totals for trends, or hourly totals to narrow down a spike. 3. Filter by product and API key to isolate an application. 4. For indexing costs, filter by resource to find the Subgraph responsible. 5. Use event-type filters to separate different kinds of work within a product. After rotating a key, use the revocation timestamp in the API key dropdown to distinguish the old key from its replacement. Revoked keys show their name followed by `(Revoked …)`, with the exact date, time, and your time zone, and remain selectable so you can inspect their historical usage. ## Read usage times Usage may take a few minutes to appear after work completes. The current hour and day are still in progress, so their totals can change. Use completed periods when comparing consumption across days. Hourly and daily totals follow UTC boundaries. Times from the past week show relative to now, such as **2 hours ago**, and older ones show the date. Hover over, focus, or tap a time to see its full date, time, and time zone. If activity is missing, check the date range and filters. If it still does not appear, contact [GraphOps support](./support-and-status) with your organization, product, API key label, approximate UTC time, and request IDs. Include your local time zone too. ## Control spending * Estimate a workload with the service's current rates before running it at scale. * Cap retries and avoid repeating completed requests. Successful retries can count as additional work. * Split large backfills into ranges you can pause, and check usage after the first range. * Set [low-credit alerts](./billing-and-credits#get-notified-before-credits-run-low) so the right people hear before the balance runs out, and cap automatic top-ups with a [monthly spending limit](./billing-and-credits#automatic-top-ups). ## Plan for more traffic Limits apply across all callers in your organization, not per API key, and they do not change with your plan size. Check your service's usage page and your balance before increasing traffic, concurrency, or backfill size. Each organization can also have up to 50 active API keys and 10 Managed Subgraphs by default; see [organization limits](./billing-and-credits#organization-limits). | Question | Where to find the answer | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | How many credits remain? | [Billing](https://platform.graphops.xyz/organizations/current/billing) in the platform. | | Which workloads use them? | [Usage](https://platform.graphops.xyz/organizations/current/usage) in the platform. | | What does each service charge? | The service usage pages in the next steps below. | | How much traffic can I send? | Your service's usage page. Contact support for a limit increase. | | How do I avoid running out mid-month? | Size your [monthly credits](./billing-and-credits#choose-your-monthly-credits) to your usage and turn on [automatic top-ups](./billing-and-credits#automatic-top-ups). | ## Next steps * [RPC usage and limits](/rpc/usage-and-limits): Check request rates, batches, WebSockets, and charges. * [Subgraphs usage and limits](/subgraphs/usage-and-limits): Check query and managed indexing charges. * [Substreams usage and limits](/substreams/usage-and-limits): Check stream and output charges. * [Billing and credits](/platform/billing-and-credits): Size your monthly credits, add credits automatically, and set alerts. --- # Identifiers and Endpoint Aliases Source: https://docs.graphops.xyz/subgraphs/concepts/identifiers-and-aliases Every Subgraph query names a version, directly or indirectly. Choose who moves your application to a new version, and whether that move waits until the new version is indexed. The choice decides whether a release can break your queries or briefly leave them without data. If you want a recommendation rather than the full model, start with [Choose your Subgraph setup](../choose-a-setup). ## Versions and indexing A Subgraph changes through versions. Each version is a deployment, identified by its Deployment CID: the content identifier of its manifest, schema and mappings on IPFS. Publishing a change always produces a new Deployment CID. A deployment has no data until it is indexed. Indexing processes the chain from the deployment's start block up to the network head; how long that takes depends on the block range and the work its mappings do. Until a new version catches up, queries to it can fail, return fewer entities, or report an older block in `_meta`. Who indexes a deployment depends on how you use it: * When the deployment belongs to one of your organization's Managed Subgraphs, GraphOps keeps it indexed for you. * Otherwise, a query is answered only if an indexer on The Graph Network is serving that deployment. Indexers choose which deployments they serve. A newly published version may not be served yet, and an older version can stop being served after its owner publishes a new one. ## Identifier types | Identifier | Endpoint segment | What it serves | Who changes it | | ------------------------- | -------------------------------- | -------------------------------------------- | ------------------------------------------------------- | | Deployment CID | `deploymentCID/` | One exact deployment. | No one; the deployment is immutable. | | Graph Network Subgraph ID | `subgraphId/` | The version currently published for that ID. | The Subgraph owner, by publishing on The Graph Network. | | Endpoint Alias | `alias/` | Whatever its Target type selects. | Your organization, in the GraphOps platform. | In the GraphOps platform, each identifier is labeled by its role. A Managed Subgraph shows its **Current CID** and, while one is being indexed, its **Next CID**. An Endpoint Alias shows its **Serving CID**, the deployment that answers its queries now. A Graph Network Subgraph ID is labeled **Subgraph ID**. ## Deployment CID A Deployment CID identifies one immutable Subgraph deployment. Use it to keep the code and schema version fixed. Its indexed data still changes as new blocks arrive; a CID does not freeze query results. ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ ``` Deployment CIDs are useful for: * Verifying a migration against one exact deployment. * Testing a new version while it indexes, before anything serves it to your users. * Holding application behavior stable during a release, and recording rollback targets. A pinned deployment only answers while something indexes it. If you rely on indexers on The Graph Network, check that the deployment is still served after its owner publishes a new version. ## Graph Network Subgraph ID A Graph Network Subgraph ID identifies a Subgraph on The Graph Network. Its owner can upgrade it at any time by publishing a new version, and the ID then points to the new Deployment CID. ```text https://subgraphs.graphops.xyz/v1/subgraphId/ ``` The `subgraphId` endpoint serves the version the ID points to now. It does not wait for that version to be indexed, and it does not consult your Managed Subgraphs: * Right after an upgrade, queries can fail with [no indexer serving the deployment](../troubleshooting#subgraph-unavailable-no-indexer-is-serving-the-deployment), or return incomplete data while the new version catches up. * The new version can change the schema, so queries that worked before can return GraphQL validation errors. Use a Graph Network Subgraph ID when you want the owner's latest version and your application can tolerate both effects. For a production endpoint, use an Endpoint Alias with the **Latest available version** target behavior instead: it keeps serving the previous version until indexers can serve the new one. ## Endpoint Alias An Endpoint Alias is an organization-owned name for a query endpoint. Its URL stays the same while your organization chooses its **Target type**: * A **Deployment CID** target stays on that exact deployment until you change the target. Its code and schema are fixed; indexed data can still change. * A **Graph Network Subgraph ID** target follows the versions published for that ID on The Graph Network. * A **Managed Subgraph** target serves a version of a Managed Subgraph that your organization indexes. Graph Network Subgraph ID and Managed Subgraph targets also have a **Target behavior** that decides which of the target's versions serves: | Target type | Target behavior | Serves | | ------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Graph Network Subgraph ID | Latest available version | The newest published version that indexers on The Graph Network can serve. While a new version is still being indexed, the previous version keeps serving. | | Graph Network Subgraph ID | Latest published version | Each new version as soon as its owner publishes it, even before indexers can serve it. | | Managed Subgraph | Current version | The serving Current version. It moves when a ready Next version is promoted, through a manual upgrade or automatic following. | | Managed Subgraph | Next version | The Next version while it's being indexed, including partially indexed data. Without a Next version that can serve, it falls back to the Current version. | Latest available version moves to a new version once an indexer serving it on The Graph Network reports it healthy and close to the chain head. After it moves, it stays on that version even if indexers later fall behind. Both Graph Network Subgraph ID behaviors follow schema changes chosen by the Subgraph owner. The Endpoint Alias name is scoped to the organization associated with the API key. ```text https://subgraphs.graphops.xyz/v1/alias/ ``` _An Endpoint Alias keeps one URL while your organization chooses its target and target behavior. A Deployment CID target never moves on its own; a Managed Subgraph target serves its Current or Next version; a Graph Network Subgraph ID target follows the network._ - Deployment CID: Stays on one deployment until you change the target. - Managed Subgraph: Current version moves when a ready Next version is promoted; Next version serves the version being indexed. - Graph Network Subgraph ID: Latest available version moves once indexers serve a new publication; Latest published version moves at once. Target a Managed Subgraph when endpoint changes should wait for its promotion checks. Promotion requires a healthy version that meets indexing readiness checks; it does not guarantee schema compatibility. Manual upgrades can accept breaking changes, and following a Graph Network Subgraph ID authorizes them automatically. Test affected clients before enabling following or promoting a manual upgrade. Use a second Endpoint Alias with the Next version behavior as a staging endpoint for those tests. Use a Deployment CID target when your team wants to change deployments manually. Retargeting an Endpoint Alias doesn't change your application's URL, so you can start with one Target type and change it later. ## Managed Subgraph versions A Graph Network Subgraph ID points to one deployment at a time; when it moves, the previous version stops being what it serves. A Managed Subgraph holds two versions instead: * **Current version** serves queries. * **Next version** is a candidate that GraphOps indexes alongside Current. Next becomes Current only when it is healthy, has caught up close to the network head and is promoted. Promotion checks indexing readiness; it does not guarantee schema compatibility. A Managed Subgraph gets its Next version in one of two ways, set by its Target type: * With a **Deployment CID** target, your team stages each Next version and chooses when to promote it. * With a **Graph Network Subgraph ID** target, each version the owner publishes becomes Next and is promoted automatically when it is ready, including breaking schema changes. Two setups both follow new versions and wait until they are indexed: an Endpoint Alias with a Graph Network Subgraph ID target and the Latest available version target behavior, which waits for indexers on The Graph Network, and an Endpoint Alias with the Current version of a Managed Subgraph that follows the ID, which waits for GraphOps to index and promote it. Querying a following Managed Subgraph's Graph Network Subgraph ID through the `subgraphId` endpoint doesn't wait for its promotion; query through the Endpoint Alias instead. See [Managed Subgraphs](../managed-subgraphs) for the upgrade procedure. ## What happens when a new version is published Suppose the owner publishes version 2 of a Subgraph you query. Each setup moves at a different time: | Setup | When it serves version 2 | While version 2 indexes | If version 2 is invalid | | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------- | | `deploymentCID` endpoint, or Endpoint Alias with a Deployment CID target | When your team changes the CID or target. | Keeps serving version 1 while it is still indexed. | Unaffected. | | `subgraphId` endpoint | Immediately. | Can fail or return incomplete data. | Points to it anyway. | | Endpoint Alias with a Graph Network Subgraph ID target, Latest published version | Shortly after it is validated. | Can fail or return incomplete data. | Keeps version 1 and shows a tracking error. | | Endpoint Alias with a Graph Network Subgraph ID target, Latest available version | Once an indexer on The Graph Network serves it healthy and close to the chain head. | Keeps serving version 1. | Keeps version 1 and shows a tracking error. | | Endpoint Alias with the Current version of a Managed Subgraph that follows the ID | After version 2 is ready and promoted. | Keeps serving version 1. | Keeps version 1 and shows a tracking error. | | Endpoint Alias with the Current version of a Managed Subgraph with a Deployment CID target | After your team stages, tests and promotes version 2. | Keeps serving version 1. | Staging reports the problem. | | Endpoint Alias with the Next version of either Managed Subgraph | As soon as version 2 is staged and can serve queries, while it is still indexing. | Serves version 2's partially indexed data. | Falls back to version 1. | Validation checks the new version's manifest, such as its schema and its single network; it does not check your queries against the new schema. Every setup that moves automatically can deliver breaking schema changes chosen by the owner. Queries are charged at the managed query rate when they resolve to one of your Managed Subgraphs' deployments, and at the standard rate otherwise. Only Managed Subgraphs accrue indexing charges. See [Usage and limits](../usage-and-limits). ## Apply the model * Give production clients an Endpoint Alias, so a change of setup never requires an application release. * Use an Endpoint Alias with a Managed Subgraph target when queries must keep working through upgrades. * Treat a change to an Endpoint Alias with a Deployment CID target like an application release. * Prefer Deployment CIDs for migration tests and rollback targets. * Choose manual Managed Subgraph upgrades when your team must test each release before promotion. * Enable following, or target a Graph Network Subgraph ID, only when your clients can accept releases chosen by the Subgraph owner, including breaking schema changes. * Check the [`X-Deployment-CID` response header](../api#response-headers) or `_meta.deployment` to see which deployment served a request. ## Next steps * [Choose your Subgraph setup](../choose-a-setup): Pick a setup for your application from a few questions. * [Manage Endpoint Aliases](../guides/manage-aliases): Create an Endpoint Alias and see when each target type moves. * [Managed Subgraphs](../managed-subgraphs): Run indexing and control promotions for a Subgraph. --- # Schemas and queries Source: https://docs.graphops.xyz/subgraphs/concepts/schemas-and-queries Write queries that keep working as your data grows. Every Subgraph has its own GraphQL schema, and GraphOps serves it through the standard Subgraph query interface, so the entity names, filters, and ordering fields you can use come from the Subgraph, not from GraphOps. ## Schema model A Subgraph schema defines entities and relationships. For example, a decentralized exchange Subgraph may expose entities such as `pool`, `token`, `swap`, `position`, or `factory`. A governance Subgraph may expose proposals, votes, delegates, and token holders. Use introspection or the schema published with the Subgraph to discover the exact entity names, fields, filters, and relationships for the Subgraph you query. ## Query features GraphOps supports standard Subgraph GraphQL query behavior for the Subgraphs it serves: | Feature | Behavior | | ---------------- | --------------------------------------------------------------------------------------- | | Entity selection | Query entity collections and single entities exposed by the Subgraph schema. | | Filtering | Use standard generated `where` filters for fields supported by the schema. | | Ordering | Use `orderBy` and `orderDirection` when generated for the entity. | | Pagination | Use `first` and `skip`; prefer cursor-like pagination by stable fields for large scans. | | Metadata | Query `_meta` for deployment, block, and indexing-error metadata. | | Introspection | Use GraphQL introspection when enabled for the Subgraph endpoint. | ## Design production queries Good production queries: * Request only the fields the application needs. * Page through large entity sets. * Use stable ordering. * Include `_meta` when the application needs to display freshness or diagnose indexing lag. * Treat GraphQL errors as partial or failed application responses, not transport success. ## Next steps * [Query a Subgraph](../guides/query-a-subgraph): Send a parameterized query and handle the result. * [Paginate results](../guides/paginate-results): Walk a large collection with a stable ordering field. * [Schema reference](../schema-reference): Discover a Subgraph's entities, filters, and `_meta` fields. --- # Sync and reorgs Source: https://docs.graphops.xyz/subgraphs/concepts/sync-and-reorgs Decide when a Subgraph result is fresh enough to show and final enough to act on. A Subgraph indexes chain data over time and serves queries from the latest indexed block, which can trail the chain head. GraphOps Subgraphs are reorg-aware, so recently indexed data can be corrected when the chain reorganizes. _Queries are served from the highest indexed block, which _meta.block.number reports. Recently indexed data can still be corrected by a reorganization, so compare it with the network's finality before treating it as irreversible._ - _meta.block.number: block N in this example. - Chain head: block N+2 in this example. ## Metadata query Use `_meta` to inspect the indexed block and indexing-error state: ```graphql query SyncStatus { _meta { deployment block { number hash } hasIndexingErrors } } ``` Output ```json { "data": { "_meta": { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285620, "hash": "0x7931a9fc337c615e67690a3083685a5680a72653718c13451f7697ea856b14f3" }, "hasIndexingErrors": false } } } ``` The deployment that served this request Highest indexed block; track it over time Include in support requests Alert when true ## Freshness model | Signal | Meaning | | ------------------------- | ------------------------------------------------------------------------------------------------------ | | `_meta.block.number` | Highest indexed block served by the Subgraph endpoint. | | `_meta.block.hash` | Hash of the indexed block. Use it for diagnostics. | | `_meta.deployment` | Deployment serving the query. Useful when testing Endpoint Alias or Graph Network Subgraph ID changes. | | `_meta.hasIndexingErrors` | Whether indexing errors are present for the deployment. | ## Reorg-aware behavior Reorg-aware indexing means the served entity state can be corrected after a chain reorganization. Applications should: * Treat recently indexed data as potentially revisable. * Use `_meta.block.number` and `_meta.block.hash` in logs for support and debugging. * Avoid assuming that a newly indexed entity is final for settlement or irreversible user-facing state. * Compare the indexed block with your network's finality information before treating a result as irreversible; elapsed time alone does not establish finality. ## Apply the model * Monitor `_meta` on health checks and critical queries. * Log the endpoint mode: Deployment CID, Graph Network Subgraph ID, or Endpoint Alias. * After changing an Endpoint Alias target, confirm `_meta.deployment` matches the expected deployment. * If `_meta.hasIndexingErrors` is true, inspect query results carefully and contact [GraphOps support](https://platform.graphops.xyz/organizations/current/overview?support=open). ## Next steps * [Monitor sync health](../guides/monitor-sync-health): Build a `_meta` health check with lag and error alerts. * [Manage Endpoint Aliases](../guides/manage-aliases): Confirm the serving deployment after a target change. * [Troubleshooting](../troubleshooting): Diagnose stale results and indexing errors. --- # Deploy your own Subgraph Source: https://docs.graphops.xyz/subgraphs/guides/deploy-with-graph-cli Build your Subgraph with the standard Graph CLI, deploy it to get a Deployment CID, and publish it on The Graph Network if you want a Graph Network Subgraph ID. Either identifier is what you give GraphOps when you choose **Set up a Subgraph** and **My own Subgraph**. *You build and publish with Graph CLI. The Deployment CID it prints is what you query, give to a Managed Subgraph, or set as an Endpoint Alias target.* - Your project: `graph codegen · graph build`. - Publish: `graph deploy` prints the Deployment CID. - Query endpoint: /v1/deploymentCID/. - Managed Subgraph: GraphOps keeps it indexed. - Endpoint Alias: A stable URL that targets it. ## Prerequisites * A Subgraph project with `subgraph.yaml`, for example one created with `graph init`. * The standard Graph CLI installed for your project. * A Subgraph in Subgraph Studio, and its deploy key, to deploy to. * A GraphOps organization with Subgraphs access and a Subgraphs API key. ## Deploy ### Generate types \[#generate-types] ```sh graph codegen ``` ### Build the Subgraph \[#build-the-subgraph] ```sh graph build ``` ### Deploy a version \[#deploy-a-version] Authenticate once with `graph auth` and your deploy key, then deploy with the Subgraph's slug. To deploy to another Graph Node instead, add its `--node` and `--ipfs` URLs. Prerequisites 1. Replace `` with your Subgraph's name. ```sh graph deploy --version-label v1.0.0 ``` Copy the Deployment CID from the command output and record it with the release. A Deployment CID identifies this exact version. It is all a Managed Subgraph needs: GraphOps can index it whether or not you publish it. ### Publish on The Graph Network (optional) \[#publish-on-the-graph-network-optional] Publish when you want indexers on The Graph Network to serve your Subgraph, or want each version you publish to reach your application automatically: ```sh graph publish ``` Publishing gives the Subgraph a Graph Network Subgraph ID. Every later version you publish belongs to the same ID, so an ID follows your releases while a Deployment CID stays on one version. ### Use the identifier in GraphOps \[#use-the-identifier-in-graphops] Start **Set up a Subgraph** and answer **My own Subgraph**. Paste the identifier that matches how you release: [Set up a Subgraph](https://platform.graphops.xyz/organizations/current/services/subgraphs/setup) | You paste | You release new versions | See | | ------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | | Deployment CID | From GraphOps Platform: stage, test and promote each version | [Develop and release a Subgraph](./develop-and-release) | | Graph Network Subgraph ID | By publishing on The Graph Network | [Release by publishing](../choose-a-setup#release-by-publishing) | The setup then asks whether GraphOps should keep it indexed and creates the Managed Subgraph and Endpoint Alias your answers need. Managed Subgraphs need a monthly plan for your organization. ### Check the deployment \[#check-the-deployment] Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with the Deployment CID to query. [Example project](/examples/subgraphs.guides.deploy-with-graph-cli.sh-5.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/deploymentCID/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query DeploymentStatus { _meta { deployment block { number hash } hasIndexingErrors } }" }' ``` Output ```json { "data": { "_meta": { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } } } ``` Should match the CID from graph deploy Rises as the new deployment indexes Must stay false before you release `_meta.block.number` rises while the new deployment indexes. Query your application's fields only after it reaches the blocks your application needs. ## Release the new version Test the new deployment through its Deployment CID endpoint before production clients use it. Then release it as the Next version of a Managed Subgraph, so your production Endpoint Alias moves only once it is indexed; [Develop and release a Subgraph](./develop-and-release) walks through that workflow. * Keep the previous Deployment CID as a rollback target. * Check `_meta.hasIndexingErrors` after deploying and after each release. ## Next steps * [Choose your Subgraph setup](../choose-a-setup): Decide how GraphOps keeps your Subgraph indexed and how new versions reach your application. * [Develop and release a Subgraph](./develop-and-release): Test each version, then release it behind a stable URL. * [Monitor sync health](./monitor-sync-health): Track indexing progress and errors from your application. --- # Develop and release a Subgraph Source: https://docs.graphops.xyz/subgraphs/guides/develop-and-release Build your Subgraph through as many versions as you need while your production application keeps serving the version you released. You test each candidate through its own Deployment CID, and your application moves only when you promote a candidate that is indexed and ready. This guide uses one Managed Subgraph as your release pipeline: its Current version serves production, and its Next version is the candidate you are testing. ## Prerequisites * An Owner or Developer role in a GraphOps organization with Subgraphs access. * The requirements to [start indexing](../managed-subgraphs#start-indexing): available credits, room in your plan and a supported network. * A Subgraph project you can build and deploy with Graph CLI, as in [Deploy your own Subgraph](./deploy-with-graph-cli). * A Subgraphs API key. ## Set up once ### Create the Managed Subgraph \[#create-the-managed-subgraph] Deploy your first version with Graph CLI and record its Deployment CID. Create a Managed Subgraph with a **Deployment CID** target from that CID, so each release waits for your approval. ### Create the production Endpoint Alias \[#create-the-production-endpoint-alias] Create an Endpoint Alias named for your application, such as `orders`, with a **Managed Subgraph** target. You can add it in the last step of creating the Managed Subgraph. Configure your production application with its URL: ```text https://subgraphs.graphops.xyz/v1/alias/ ``` ### Optionally, create a development Endpoint Alias \[#optionally-create-a-development-endpoint-alias] If your development or staging environment should keep one URL across candidates, create a second Endpoint Alias, such as `orders-dev`, with a **Deployment CID** target. You point it at each candidate as you stage it. ## Test a new version 1. Deploy the change with Graph CLI and record the new Deployment CID. 2. On the Managed Subgraph, choose **Start upgrade** and enter the CID. GraphOps checks it as you enter it; continue once it passes, then review the schema comparison, which lists changes that can break your existing queries. 3. Query the candidate while it indexes through its Deployment CID endpoint, or through your development Endpoint Alias after you point it at the CID with **Change target**. Check `_meta.block.number` to see how far it has indexed; results are incomplete until it catches up. 4. If you find a problem, choose **Cancel upgrade**, deploy a fix and stage the new CID. A Managed Subgraph has one Next version at a time, so point your development Endpoint Alias at the new candidate too. Your production Endpoint Alias keeps serving Current throughout. Each staged version indexes from its start block and accrues indexing charges while it is staged, alongside Current. ## Release the version 1. Update and deploy any clients affected by schema changes before you release. 2. Wait until Next shows **Ready to promote**. It must be healthy and close to the network head. 3. Choose **Promote version**. Your production Endpoint Alias moves to the new version, and your application keeps the same URL. 4. Query `_meta.deployment` through the production Endpoint Alias to confirm the CID it serves. Record each released Deployment CID. The previous version is removed after promotion. ## Roll back Returning to a previous version is another upgrade, not an instant switch. Stage the previous Deployment CID as Next, let it index, and promote it when it is ready. Plan for that indexing time in your release process, and fix forward when a corrected version would be ready sooner. ## Keep a separate development environment One Managed Subgraph is enough when you test one candidate at a time. Create a second Managed Subgraph for development when you need a development version that keeps running, such as for a long-lived staging environment, while production upgrades separately: * Point your development Endpoint Alias at the development Managed Subgraph with a **Managed Subgraph** target, and promote there as often as you like. * To release, stage the tested Deployment CID on the production Managed Subgraph and promote it when it is ready. Each Managed Subgraph uses its own plan slot and accrues its own indexing charges, even when both index the same Deployment CID. ## Release by publishing on The Graph Network If your team publishes the Subgraph on The Graph Network, you can make that publication the release. Create the production Managed Subgraph with a **Graph Network Subgraph ID** target for your Subgraph's ID. Each version you publish becomes Next and is promoted automatically when it is ready, including breaking schema changes. Because publishing releases the version, test each candidate before you publish it, for example in a development Managed Subgraph with a **Deployment CID** target. Point your production application at an Endpoint Alias that targets the production Managed Subgraph, not at the `subgraphId` endpoint, which moves to each publication before it is indexed. ## Before production * Point production clients at an Endpoint Alias, never at a candidate's Deployment CID. * Keep a record of each released Deployment CID and the schema your clients expect. * Log `_meta.deployment` with production queries so you can tell which version served a request. * Check that your credit balance covers both versions during an upgrade; see [Plan indexing costs](../managed-subgraphs#plan-indexing-costs). ## Next steps * [Managed Subgraphs](../managed-subgraphs): See status, readiness and costs for each version. * [Monitor sync health](./monitor-sync-health): Detect lag and indexing errors from your application. * [Choose your Subgraph setup](../choose-a-setup): Compare this workflow with other setups. --- # Discover Subgraphs Source: https://docs.graphops.xyz/subgraphs/guides/discover-public-subgraphs Find a public dataset you can query before writing an integration. You can check what a Subgraph indexes, explore its schema, run queries against it, and leave with a query endpoint. You need membership in a GraphOps organization. ## Find a dataset Search by name and filter by indexed network. [Discover Subgraphs](https://platform.graphops.xyz/organizations/current/services/subgraphs/discover) To look up a Subgraph you already know, paste its Graph Network Subgraph ID or a Deployment CID into the same search box and apply it. The page offers **Explore Subgraph** or **Explore deployment** for that identifier, even when it is not among the popular datasets. A Deployment CID must belong to a Subgraph published on The Graph Network, or to your organization's Managed Subgraphs or Endpoint Aliases. ## Choose a dataset The catalog ranks datasets by **Query popularity** over seven complete UTC days: * The most popular dataset scores 100. A score of 50 means half its recorded query fees. Searching, filtering and changing pages keep the same scale. Bars use a logarithmic display so less popular datasets are easier to compare; their lengths are not direct percentages. * IDs sharing the same current Deployment CID appear in one row. Popularity combines their published version histories, counting each historical deployment once in that row. * The row uses the name and Graph Network Subgraph ID with the highest individual popularity. Expand its identifier list to explore another ID; searching matches all member names and IDs. * Popularity does not count unique queries or guarantee that the data fits your application. Discovery includes public datasets that meet the platform's curation eligibility requirements. ## Read the metadata Open a Subgraph in the deployment explorer from its name or its row's **Explore Subgraph** action. The explorer has three sections: **Overview**, **Schema** and **Query**. On **Overview**, **Subgraph metadata** summarizes what you need before querying it, derived from the deployment's manifest: * its Graph Network Subgraph ID, current Deployment CID and indexed network; * the block it indexes from, its data sources and templates, and the contracts it reads; * its triggers, such as event, call or block handlers; * its declared query history: full history, recent blocks only, or a fixed number of blocks; * declared features such as full-text search, and the base deployment and block when it is grafted; * its manifest spec and mapping API versions, and its source repository when published. Facts the manifest does not declare are left out. ## Explore the schema Under **Schema**, the schema relationship map starts with compact nodes and connections. Its default focus is a queryable entity with the most distinct related entities; incoming relationships and useful fields break ties. For example, the Uniswap schema starts at `Pool`. * Hover over a type to highlight its direct relationships; unrelated types fade. * Check **Show fields** in the map's header to display entity fields and enum values. * Choose **Related types** to focus on the selected type and its neighbors, or **All types** to show the complete schema. * Drag to pan. Pinch on a touchscreen or trackpad to zoom, or use **Ctrl + scroll**, the zoom buttons or **Fit**. * Choose **Full screen** beside the zoom buttons to expand the map to the whole window. Choose **Exit full screen** or press **Esc** to return. A type's details open above the full-screen map, and closing them returns to it. * Search by type, field or description to find the data you need. * Select a type to focus its relationships, or click a relationship field's type to refocus on that entity. Reduced-motion preferences move immediately. Type signatures show nullability (`!`) and lists (`[]`). Connections show required references (`1`), optional references (`0..1`) and lists (`0..*`); lists may be empty even when their type is required. Choose a type's eye icon or **Details** to open a dialog with full field descriptions and enum values, and to navigate to related entities. The dialog header shows entity annotations beside the type name, such as `Token @entity` or `GraphNetwork @entity(immutable: false)`. These come from the deployment's authoring schema. An annotation without arguments stays as declared; the explorer does not infer defaults. Close the dialog to return to the diagram. For an entity with its own collection query, choose **Query records** to open **Query** with a query for its first records. ## Query the Subgraph Under **Query**, write any read-only GraphQL query for the deployment you are exploring and choose **Run query**, or press \**Ctrl+Enter*\* (**⌘+Enter** on macOS). * Choose an entity in **Start from entity** to start from a query for its first ten records and their scalar fields, then edit it. * The query is checked against the deployment's schema as you type. Each problem shows its line and column; choose the position to move the cursor there. Misspelled fields and arguments include a suggestion, such as `Did you mean "name"?`. **Run query** is available once the query has no problems. * Enter **Variables** as a JSON object. When a query defines several operations, choose the **Operation** to run. * **Prettify** reformats the query. * **Result** shows the response's `data` and any `errors` from the deployment, and can be copied. Only query operations can run. A query can be at most 16 KB and nest fields at most eight levels deep. Results come from the Deployment CID you are exploring; links to a Deployment CID continue to query that version. Explorer queries are free within an hourly allowance for each organization. After each run, the panel shows how many **Free queries** remain this hour. If the allowance is used up, retry after it resets, or query through [an endpoint](#get-an-endpoint) of your own, which uses your organization's normal billing. If the explorer reports that no indexer is serving the deployment, follow the [indexing coverage checks](../troubleshooting#subgraph-unavailable-no-indexer-is-serving-the-deployment) before retrying. If it reports that every indexer has an indexing error, see [indexers report an indexing error](../troubleshooting#subgraph-unavailable-indexers-report-an-indexing-error). ## Get an endpoint Choose **Get query endpoint** from a catalog row's actions or the deployment explorer's **Actions** menu: 1. Check the identifier the wizard selects, shown in the summary at the top; **Edit** changes it. From a Subgraph, it selects the Graph Network Subgraph ID, so your endpoint follows the publisher's current version, including while a new version is still being indexed. From a Deployment CID, it selects that exact deployment. For a production application, see [Choose your Subgraph setup](../choose-a-setup). 2. Supply a saved API key, or create one if you are an Owner or Developer. Viewers and billing admins can browse and query but cannot create keys. 3. Copy the endpoint and authentication instructions into your application. Queries through that endpoint use your organization's normal billing. Closing the wizard returns you to the catalog or detail page you started from, with its catalog filters preserved. ## Keep the Subgraph in your organization If you are an Owner or Developer, you can start from the deployment explorer instead of entering the identifier again. Open the explorer's **Actions** menu: * **Set up a Subgraph** asks how new versions should reach your application, recommends one of the setups in [Choose your Subgraph setup](../choose-a-setup), and creates it. * **Create Endpoint Alias** gives your application a stable query URL. Its target is the identifier you are exploring: a Graph Network Subgraph ID follows the publisher's new versions, and a Deployment CID stays on that exact version. * **Create Managed Subgraph** keeps the Subgraph indexed for your organization and needs a monthly plan. A Graph Network Subgraph ID follows published versions; a Deployment CID is upgraded when you choose. Each one starts with that identifier, which you can change before you create. Creating an Endpoint Alias or a Managed Subgraph also lets you change its **Target type**. Closing returns you to the explorer. After you create a Managed Subgraph, its page opens. See [Manage Endpoint Aliases](./manage-aliases) and [Managed Subgraphs](../managed-subgraphs) for what each target type does. ## Explore your own resources You can also choose **Explore deployment** from a Managed Subgraph version's actions, or explore an Endpoint Alias from its row actions: * Version links, and Endpoint Aliases that target a Deployment CID or a Managed Subgraph, inspect that Deployment CID. The endpoint wizard selects a Deployment CID endpoint. * An Endpoint Alias that follows a Graph Network Subgraph ID opens **Explore Subgraph** for that ID's current version. The breadcrumb takes you back to the originating Subgraphs page. ## Next steps * [Query a Subgraph](./query-a-subgraph): Send your query from your application and check its freshness. * [Paginate results](./paginate-results): Read beyond the first page of results in bounded pages. * [Identifiers and Endpoint Aliases](../concepts/identifiers-and-aliases): Decide whether to follow the publisher or pin a deployment. --- # Connect an existing Subgraph Source: https://docs.graphops.xyz/subgraphs/guides/import-existing-subgraph Query a public Subgraph through GraphOps without changing its schema or your GraphQL queries. The one decision you make is who controls the version you query: pin a Deployment CID to stay on one version, or use a Graph Network Subgraph ID to follow the owner's upgrades. ## Prerequisites * A GraphOps Platform account. * An organization with access to Subgraphs. * A public Deployment CID or Graph Network Subgraph ID. * A Subgraphs API key. ## Connect ### Choose the identifier \[#choose-the-identifier] Choose one identifier: | Identifier | Use it when | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Deployment CID | You want one exact deployment and schema. | | Graph Network Subgraph ID | You want the endpoint to follow upgrades on The Graph Network, and can accept failed or incomplete queries while each new version is indexed. | For a Subgraph you control, the standard Graph CLI deployment output includes the Deployment CID after publishing. For a public Subgraph, use the identifier published by the Subgraph owner, or find it in [Discover Subgraphs](./discover-public-subgraphs). ### Choose the query endpoint \[#choose-the-query-endpoint] Use the route that matches your identifier: ```text https://subgraphs.graphops.xyz/v1/deploymentCID/ https://subgraphs.graphops.xyz/v1/subgraphId/ ``` Authenticate with your Subgraphs key as described in [Authentication](../authentication). If you want GraphOps to run indexing for your deployment, follow [Managed Subgraphs](../managed-subgraphs). Use a Deployment CID to manage releases manually, or a Graph Network Subgraph ID to index and promote future published versions automatically. ### Add a stable name if you need one \[#add-a-stable-name-if-you-need-one] Create an Endpoint Alias when application code should use a stable organization-owned name: ```text https://subgraphs.graphops.xyz/v1/alias/ ``` An Endpoint Alias with a Deployment CID target stays on that CID; a Managed Subgraph target follows its serving Current version; a Graph Network Subgraph ID target follows the latest validated publication for that ID. Choose the [Endpoint Alias target](./manage-aliases) based on who should control releases and whether endpoint changes should wait for Managed Subgraph indexing readiness. [Choose your Subgraph setup](../choose-a-setup) recommends one for production. ### Verify query access \[#verify-query-access] Send this GraphQL query to your chosen endpoint using your Subgraphs API key: ```graphql query ImportedSubgraphStatus { _meta { deployment block { number hash } hasIndexingErrors } } ``` Output ```json { "data": { "_meta": { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } } } ``` The deployment your endpoint resolved to Highest indexed block served Should be false before you build on it If `_meta.deployment` is not the deployment you expected, check the identifier and endpoint shape. A Graph Network Subgraph ID endpoint moves when the owner publishes a new version, so record the deployment you tested against. ## Before production * Confirm schema compatibility before you point an Endpoint Alias used by production clients at a new deployment. * Don't point production clients at the `subgraphId` endpoint if they need data through upgrades. It moves to each new version before indexers serve it; an Endpoint Alias that targets a Managed Subgraph waits until the new version is ready. * Use Endpoint Aliases when your organization needs controlled upgrades, rollbacks, or environment-specific endpoints. * A Deployment CID keeps the code and schema version fixed; indexed data continues to change. ## Next steps * [Query a Subgraph](./query-a-subgraph): Read entities from the endpoint and handle GraphQL errors. * [Manage Endpoint Aliases](./manage-aliases): Give production clients a stable URL you control. * [Identifiers and Endpoint Aliases](../concepts/identifiers-and-aliases): Compare who controls each identifier's version. --- # Manage Endpoint Aliases Source: https://docs.graphops.xyz/subgraphs/guides/manage-aliases Keep the same query URL when you release a new Subgraph version. An Endpoint Alias gives your application a name such as `positions` or `production`, while your organization chooses its target and when that target changes. ## Prerequisites * A GraphOps organization with Subgraphs access. * An Owner or Developer role to manage Endpoint Aliases. * A Subgraphs API key. * A Deployment CID, Graph Network Subgraph ID, or existing Managed Subgraph to target. * The schema fields your production clients use, so you can check compatibility before you retarget. ## Choose a target type The **Target type** decides when your Endpoint Alias moves to a new deployment: * **Deployment CID**: stays on that exact deployment until you change it. * **Graph Network Subgraph ID**: follows the versions published for that ID on The Graph Network, including schema changes chosen by the Subgraph owner. * **Managed Subgraph**: serves a version of a Managed Subgraph your organization indexes. ## Choose a target behavior Graph Network Subgraph ID and Managed Subgraph targets also need a **Target behavior**: * **Latest available version** (Graph Network Subgraph ID, recommended): serves the newest version that indexers on The Graph Network can serve. When the owner publishes, the previous version keeps serving until indexers catch up on the new one. It then moves and stays on the new version. * **Latest published version** (Graph Network Subgraph ID): moves as soon as the owner publishes, even if no indexer can serve the new version yet. * **Current version** (Managed Subgraph): follows the serving Current version, so it moves only after a Next version meets the readiness checks and is promoted. Use it for production. * **Next version** (Managed Subgraph): serves the Next version while it's being indexed, including partially indexed data, and falls back to the Current version when there's no Next version that can serve. Use it as a staging endpoint. *Endpoint Aliases for one published Subgraph when its owner publishes a new version: two Graph Network Subgraph ID targets with different target behaviors, a Managed Subgraph that follows the same ID, and a Deployment CID. Each moves at a different time.* - Before: Every Endpoint Alias serves the owner's published version. (Latest published version: Qm…a1f4; Latest available version: Qm…a1f4; Managed Subgraph: Qm…a1f4; Deployment CID: Qm…a1f4) - Owner publishes: Latest published moves once the release is validated, before indexers can serve it. Latest available keeps serving the previous version, and the Managed Subgraph indexes the release as Next. (Latest published version: Qm…7c2e; Latest available version: Qm…a1f4 Waiting for indexers; Managed Subgraph: Qm…a1f4 Next Syncing; Deployment CID: Qm…a1f4) - New version serves: Latest available moves once indexers serve the new version, and the Managed Subgraph target moves when its Next version is promoted. The Deployment CID target stays until you change it. (Latest published version: Qm…7c2e; Latest available version: Qm…7c2e; Managed Subgraph: Qm…7c2e; Deployment CID: Qm…a1f4) Choose Latest available version or a Managed Subgraph target when indexing readiness should control endpoint changes. Choose a Deployment CID target when your team wants to change deployments by hand. [Choose your Subgraph setup](../choose-a-setup) compares the complete setups. A typical release flow for your own Subgraph uses two Endpoint Aliases on one Managed Subgraph: a `production` alias with Current version and a `staging` alias with Next version. Staging moves to a new version as soon as it can serve queries, production moves when you promote it, and both serve the same version between releases. ## Choose and update an Endpoint Alias ### Create or choose the Endpoint Alias \[#create-or-choose-the-endpoint-alias] Choose a short name that represents the application contract, such as `uniswap-v3`, `positions`, or `production`. Use separate Endpoint Aliases, such as `positions` and `positions-dev`, when environments should move at different times. You can also create one while getting a query endpoint: in **Get query endpoint**, choose Subgraphs, then **Endpoint Alias**, and choose **Create Endpoint Alias**. It asks the same questions as creating the Endpoint Alias from the Endpoint Aliases page, described in the next step. After it's created, choose **Get query endpoint** to return with the new Endpoint Alias selected, ready to choose an API key. If you close it instead, you return to where you left off. Endpoint Alias query URL: ```text https://subgraphs.graphops.xyz/v1/alias/ ``` ### Point the Endpoint Alias to a target \[#point-the-endpoint-alias-to-a-target] [Create Endpoint Alias](https://platform.graphops.xyz/organizations/current/services/subgraphs/new) Creating an Endpoint Alias asks two questions, one at a time, before you name it: 1. What it follows: choose its [**Target type**](#choose-a-target-type), then enter the Deployment CID or Graph Network Subgraph ID, or select the Managed Subgraph. 2. How it moves to new versions: choose its [**Target behavior**](#choose-a-target-behavior) from the ones that target type offers. A Deployment CID target has none to choose; it stays on that deployment. Then name it and check the query URL the name gives. Review shows the target, its target behavior and the Deployment CID the Endpoint Alias will serve, and nothing is created until you confirm there. A Managed Subgraph target needs an active Managed Subgraph in your organization; until you have one, choose **Set up a Subgraph** from that option's explanation to create one. While you [explore a Subgraph](./discover-public-subgraphs#keep-the-subgraph-in-your-organization), you can instead choose **Create Endpoint Alias** to start with its identifier. On a Managed Subgraph's page, it starts with that Managed Subgraph as the target. To change an existing Endpoint Alias, choose **Change target** from its row actions. It asks the same two questions. Before you save, it shows what the Endpoint Alias serves now and after saving. A Deployment CID, Latest published version or Current version target switches queries as soon as you save, so confirm the new deployment answers first. A Latest available version or Next version target switches once that version can serve queries. After you save, it shows the Deployment CID the Endpoint Alias serves and, for a Latest available version alias that is waiting, the latest version and why it isn't serving yet. Every target must resolve to a valid single-network deployment. If a Graph Network Subgraph ID target can't be resolved later, the Endpoint Alias keeps its last mapping and shows a tracking error. ### Verify the target \[#verify-the-target] The **Target type** column shows each Endpoint Alias's target type and target behavior; their info icons explain them. The **Target** column shows the Graph Network Subgraph ID or Managed Subgraph the Endpoint Alias follows and its **Serving CID**, the Deployment CID that answers its queries. When that isn't the version its target behavior names, a note says which version it is, such as Previous version. When a Latest available version alias is waiting for indexers, the column also shows the **Latest CID** and why it isn't serving yet. Use the row's actions to explore the target's schema or get its query endpoint. Then query `_meta` through the Endpoint Alias: ```graphql query AliasTarget { _meta { deployment block { number hash } hasIndexingErrors } } ``` Output ```json { "data": { "_meta": { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } } } ``` Should match the Serving Deployment CID Should keep rising on the new target Roll back if this turns true ### Roll back if needed \[#roll-back-if-needed] For an Endpoint Alias with a Deployment CID target, change its target to the previous Deployment CID after confirming that deployment is queryable. Keep previous CIDs in your release records. For an Endpoint Alias with a Managed Subgraph target, prepare and promote the previous deployment as another [Managed Subgraph upgrade](../managed-subgraphs), or explicitly convert the Endpoint Alias to a Deployment CID target. A Managed Subgraph upgrade must meet the readiness checks before promotion. To stop an Endpoint Alias from following Graph Network publications, convert it to a Deployment CID target. A Latest available version alias doesn't move back to an earlier version on its own; to return to one, change it to a Deployment CID target. ## Release safely * Treat Endpoint Alias updates as application releases, and retarget during low-risk windows if schema changes can affect live traffic. * Confirm required entity names, fields, filters, and ordering fields before you retarget. * Keep previous Deployment CIDs available as rollback targets. * Log the [`X-Deployment-CID` response header](../api#response-headers) or `_meta.deployment` in production diagnostics so support can see which deployment served a request. ## Next steps * [Managed Subgraphs](../managed-subgraphs): Prepare and promote a Next version behind a Managed Subgraph target. * [Monitor sync health](./monitor-sync-health): Confirm the serving deployment and its freshness after each change. * [Identifiers and Endpoint Aliases](../concepts/identifiers-and-aliases): Compare Endpoint Aliases with direct identifiers. --- # Monitor sync health Source: https://docs.graphops.xyz/subgraphs/guides/monitor-sync-health Detect stale data before it reaches your users. Run a small `_meta` query on a schedule, or add `_meta` to important reads, to see which deployment served the request, how far it has indexed, and whether it reports indexing errors. ## Prerequisites * A Subgraphs API key. * A query endpoint using a Deployment CID, Graph Network Subgraph ID, or Endpoint Alias. * A place to record health-check results, such as application logs or metrics. * For lag alerts, a reliable source of the chain head for the same network. ## Request This request reads `_meta` through an Endpoint Alias: the serving deployment, the indexed block, and the indexing-error flag. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with your Endpoint Alias. [Example project](/examples/subgraphs.guides.monitor-sync-health.sh-1.tar.gz) ```sh # `_meta` reports the serving deployment, its latest indexed block, and any indexing errors. curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/alias/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query SyncHealth { _meta { deployment block { number hash } hasIndexingErrors } }", "operationName": "SyncHealth" }' ``` Output ```json { "data": { "_meta": { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } } } ``` The deployment that served this request Highest indexed block; track it over time Include in support requests Alert when true Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/subgraphs.guides.monitor-sync-health.ts-2.tar.gz) ```typescript export {}; // `_meta` reports the serving deployment, its latest indexed block, and any indexing errors. const response = await fetch( "https://subgraphs.graphops.xyz/v1/alias/", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: ` query SyncHealth { _meta { deployment block { number hash } hasIndexingErrors } } `, operationName: "SyncHealth", }), }, ); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); console.log(JSON.stringify(result.data._meta)); ``` Output ```json { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } ``` The deployment that served this request Highest indexed block; track it over time Include in support requests Alert when true Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` with your Endpoint Alias. Tested with Go 1.26.0. [Example project](/examples/subgraphs.guides.monitor-sync-health.go-5.tar.gz) ```go package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { // `_meta` reports the serving deployment, its latest indexed block, and any indexing errors. payload, err := json.Marshal(struct { Query string `json:"query"` OperationName string `json:"operationName"` }{ Query: ` query SyncHealth { _meta { deployment block { number hash } hasIndexingErrors } } `, OperationName: "SyncHealth", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, "https://subgraphs.graphops.xyz/v1/alias/", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } var result struct { Data struct { Meta json.RawMessage `json:"_meta"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { log.Fatal(err) } var meta bytes.Buffer if err := json.Compact(&meta, result.Data.Meta); err != nil { log.Fatal(err) } fmt.Println(meta.String()) } ``` Output ```json { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } ``` The deployment that served this request Highest indexed block; track it over time Include in support requests Alert when true Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/subgraphs.guides.monitor-sync-health.rust-6.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { // `_meta` reports the serving deployment, its latest indexed block, and any indexing errors. let response = reqwest::Client::new() .post("https://subgraphs.graphops.xyz/v1/alias/") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "query": r#" query SyncHealth { _meta { deployment block { number hash } hasIndexingErrors } } "#, "operationName": "SyncHealth", })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; println!("{}", result["data"]["_meta"]); Ok(()) } ``` Output ```json { "block": { "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881", "number": 26285621 }, "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "hasIndexingErrors": false } ``` Include in support requests Highest indexed block; track it over time The deployment that served this request Alert when true Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/subgraphs.guides.monitor-sync-health.python-4.tar.gz) ```python import json import os import requests # `_meta` reports the serving deployment, its latest indexed block, and any indexing errors. response = requests.post( "https://subgraphs.graphops.xyz/v1/alias/", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "query": """ query SyncHealth { _meta { deployment block { number hash } hasIndexingErrors } } """, "operationName": "SyncHealth", }, timeout=30, ) response.raise_for_status() print(json.dumps(response.json()["data"]["_meta"])) ``` Output ```json { "deployment": "QmTZ8ejXJxRo7vDBS4uwqBeGoxLSWbhaA7oXa1RvxunLy7", "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" }, "hasIndexingErrors": false } ``` The deployment that served this request Highest indexed block; track it over time Include in support requests Alert when true ## Act on each signal *Run the same _meta query on a schedule and act on each signal. Check indexing errors first, then the serving deployment, then freshness.* - `hasIndexingErrors` is true: Alert and check results before you rely on them. - `deployment` is not the CID you expect: Check the Endpoint Alias target or the owner's latest release. - `block.number` stops advancing or falls behind your head source: Alert on lag and show freshness to users. - `block.hash` on every check: Log it with the block number for support requests. Every response also carries the [`X-Deployment-CID` header](../api#response-headers). Alert when it changes unexpectedly for an endpoint you expect to stay on one deployment, and record it with each health-check result. Compare `_meta.block.number` with the chain head only when you have a reliable head source for the same network. If `_meta.hasIndexingErrors` is `true`, follow the checks in [Troubleshooting](../troubleshooting#_metahasindexingerrors-is-true). Recent blocks can still be corrected by a chain reorganization. See [Sync and reorgs](../concepts/sync-and-reorgs) before you treat a result as final. ## Next steps * [Sync and reorgs](../concepts/sync-and-reorgs): Understand indexing lag and when indexed data can change. * [Manage Endpoint Aliases](./manage-aliases): Check which target an Endpoint Alias serves after a release. * [Troubleshooting](../troubleshooting): Resolve stale results, indexing errors, and query failures. --- # Paginate results Source: https://docs.graphops.xyz/subgraphs/guides/paginate-results Read every pool, holder, or vote in a collection for a report or a sync job. Order by a stable field and ask for the rows after the last `id` you received, so each request is bounded and a worker can resume where it stopped. ## Prerequisites * A Subgraphs API key. * A Subgraph endpoint that exposes the entity collection you want to scan. * A stable ordering field such as `id`, `createdAtTimestamp`, or another indexed field in the Subgraph schema. ## Choose a pagination method For a few pages, `first` and `skip` are usually enough. For long scans, filter past the last value of a stable field, such as `where: { id_gt: $lastId }` with `orderBy: id`. Each page then starts where the previous one ended, however far into the collection you are: *Each page asks for the rows after the last id of the page before it. Save that id after you process a page, and stop when a page returns fewer rows than you requested.* Each page: `pools(first: 100, where: { id_gt: $lastId }, orderBy: id, orderDirection: asc)` - Page 1: `id_gt 0x00…00`, 100 rows. - Page 2: `id_gt 0x3a…f1`, 100 rows. - Page 3: `id_gt 0x7c…02`, 37 rows · stop. ## Request This request reads one page of up to 10 pools in the current indexed view after `$lastId`. For the first page, set `$lastId` to a value that sorts before every `id`, such as the zero address in these examples. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with your Endpoint Alias. [Example project](/examples/subgraphs.guides.paginate-results.sh-1.tar.gz) ```sh # Start below every id. For the next page, set lastId to the last id in this response. curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/alias/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query PoolsPage($first: Int!, $lastId: ID!) { pools(first: $first, where: { id_gt: $lastId }, orderBy: id, orderDirection: asc) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } }", "variables": { "first": 10, "lastId": "0x0000000000000000000000000000000000000000" }, "operationName": "PoolsPage" }' ``` Output ```json { "data": { "pools": [ { "id": "0x000024feb293b6c6c3a80a95f1f830a8746400b9", "token0": { "symbol": "TRUMP" }, "token1": { "symbol": "WETH" } }, { "id": "0x0000d36ab86d213c14d93cd5ae78615a20596505", "token0": { "symbol": "RALPH" }, "token1": { "symbol": "WETH" } }, { "id": "0x00013bffe4c38c1372a450fac78b01cb4f554781", "token0": { "symbol": "Pectra" }, "token1": { "symbol": "WETH" } }, { "id": "0x0001fcbba8eb491c3ccfeddc5a5caba1a98c4c28", "token0": { "symbol": "BCZ" }, "token1": { "symbol": "WETH" } }, { "id": "0x00024b66a38a676c0eae59126c1367ad141afa79", "token0": { "symbol": "WETH" }, "token1": { "symbol": "TI" } }, { "id": "0x0002e63328169d7feea121f1e32e4f620abf0352", "token0": { "symbol": "wNXM" }, "token1": { "symbol": "ICHI" } }, { "id": "0x0003c9d75e102b8a9fe77280ca410e9135beb150", "token0": { "symbol": "TRANCHE-bWBTC-A" }, "token1": { "symbol": "USDT" } }, { "id": "0x0003e7d8d22d0e8e53d56885069fc8a254f0431f", "token0": { "symbol": "RLP" }, "token1": { "symbol": "USDC" } }, { "id": "0x0004cd8474e882278e32e584699090be496f410e", "token0": { "symbol": "ZERO" }, "token1": { "symbol": "USDC" } }, { "id": "0x0005079292756443f2e711570f8ab4e1951d4f97", "token0": { "symbol": "EDG" }, "token1": { "symbol": "WETH" } } ], "_meta": { "block": { "number": 26285621, "hash": "0x40c3154aaa484a99057c58a6c72c27171ed861d260179db387850027655d0881" } } } } ``` Up to $first rows, ordered by id Last id: your next $lastId Log with each page Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/subgraphs.guides.paginate-results.ts-2.tar.gz) ```typescript export {}; // Start below every id. For the next page, pass the last id this page returns. const lastId = "0x0000000000000000000000000000000000000000"; const response = await fetch( "https://subgraphs.graphops.xyz/v1/alias/", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: ` query PoolsPage($first: Int!, $lastId: ID!) { pools( first: $first where: { id_gt: $lastId } orderBy: id orderDirection: asc ) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } `, variables: { first: 10, lastId }, operationName: "PoolsPage", }), }, ); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); // The last id is the cursor for the next page; null means there are no more rows. console.log(JSON.stringify(result.data.pools.at(-1)?.id ?? null)); ``` Output ```json "0x0005079292756443f2e711570f8ab4e1951d4f97" ``` Use this cursor as lastId in the next request Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` with your Endpoint Alias. Tested with Go 1.26.0. [Example project](/examples/subgraphs.guides.paginate-results.go-5.tar.gz) ```go package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { // Start below every id. For the next page, pass the last id this page returns. lastID := "0x0000000000000000000000000000000000000000" payload, err := json.Marshal(struct { Query string `json:"query"` Variables map[string]any `json:"variables"` OperationName string `json:"operationName"` }{ Query: ` query PoolsPage($first: Int!, $lastId: ID!) { pools( first: $first where: { id_gt: $lastId } orderBy: id orderDirection: asc ) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } `, Variables: map[string]any{"first": 10, "lastId": lastID}, OperationName: "PoolsPage", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, "https://subgraphs.graphops.xyz/v1/alias/", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } var result struct { Data struct { Pools []struct { ID string `json:"id"` } `json:"pools"` } `json:"data"` Errors json.RawMessage `json:"errors"` } if err := json.Unmarshal(body, &result); err != nil { log.Fatal(err) } // GraphQL errors can arrive with HTTP 200; a missing page must not read as the last one. if result.Errors != nil || result.Data.Pools == nil { log.Fatalf("GraphQL errors: %s", body) } // The last id is the cursor for the next page; null means there are no more rows. var cursor *string if pools := result.Data.Pools; len(pools) > 0 { cursor = &pools[len(pools)-1].ID } next, err := json.Marshal(cursor) if err != nil { log.Fatal(err) } fmt.Println(string(next)) } ``` Output ```json "0x0005079292756443f2e711570f8ab4e1951d4f97" ``` Use this cursor as lastId in the next request Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/subgraphs.guides.paginate-results.rust-6.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { // Start below every id. For the next page, pass the last id this page returns. let last_id = "0x0000000000000000000000000000000000000000"; let response = reqwest::Client::new() .post("https://subgraphs.graphops.xyz/v1/alias/") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "query": r#" query PoolsPage($first: Int!, $lastId: ID!) { pools( first: $first where: { id_gt: $lastId } orderBy: id orderDirection: asc ) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } "#, "variables": { "first": 10, "lastId": last_id }, "operationName": "PoolsPage", })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; let pools = result["data"]["pools"].as_array().ok_or("missing pools")?; // The last id is the cursor for the next page; null means there are no more rows. println!( "{}", pools.last().map_or(Value::Null, |pool| pool["id"].clone()) ); Ok(()) } ``` Output ```json "0x0005079292756443f2e711570f8ab4e1951d4f97" ``` Use this cursor as lastId in the next request Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/subgraphs.guides.paginate-results.python-4.tar.gz) ```python import json import os import requests # Start below every id. For the next page, pass the last id this page returns. last_id = "0x0000000000000000000000000000000000000000" response = requests.post( "https://subgraphs.graphops.xyz/v1/alias/", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "query": """ query PoolsPage($first: Int!, $lastId: ID!) { pools( first: $first where: { id_gt: $lastId } orderBy: id orderDirection: asc ) { id token0 { symbol } token1 { symbol } } _meta { block { number hash } } } """, "variables": {"first": 10, "lastId": last_id}, "operationName": "PoolsPage", }, timeout=30, ) response.raise_for_status() pools = response.json()["data"]["pools"] # The last id is the cursor for the next page; None means there are no more rows. print(json.dumps(pools[-1]["id"] if pools else None)) ``` Output ```json "0x0005079292756443f2e711570f8ab4e1951d4f97" ``` Use this cursor as lastId in the next request ## Run the full scan 1. Send the first request with `$lastId` set before every `id`. 2. Process the page, then save the last `id` you processed. 3. Send the next request with that `id` as `$lastId`. 4. Stop when a page returns fewer rows than `$first`. Keep these in mind for long scans: * Keep page sizes bounded, and back off after `429` responses. * Prefer `id_gt` filters over high `skip` values. * Persist the last processed `id` so a worker can resume after a retry or restart. * Later pages can reflect newer data than earlier ones. To read every page from the same block, pass the same `block: { number: N }` argument on each query; otherwise log `_meta.block.number` with each page. ## Next steps * [Schemas and queries](../concepts/schemas-and-queries): Find the ordering fields and filters your schema generates. * [Usage and limits](../usage-and-limits): Size pages and worker concurrency to your organization's limits. * [Monitor sync health](./monitor-sync-health): Track indexing progress while a long scan runs. --- # Query a Subgraph Source: https://docs.graphops.xyz/subgraphs/guides/query-a-subgraph Read indexed entities from your application and know how fresh they are. This guide sends a parameterized GraphQL query through an Endpoint Alias, includes `_meta` so each response reports the indexed block behind it, and checks for GraphQL errors that can arrive with HTTP `200`. The example requires `pools`, `token0`, `token1`, and `totalValueLockedUSD` fields from a Uniswap-style schema. If your Subgraph uses a different schema, adapt the selection using [Schema reference](../schema-reference). The same request works with a Deployment CID or Graph Network Subgraph ID endpoint. ## Prerequisites * A Subgraphs API key. * A Deployment CID, Graph Network Subgraph ID, or Endpoint Alias for the Subgraph you want to query. * The GraphQL schema for that Subgraph, or introspection access to discover it. ## Request This request reads the five pools with the highest total value locked in the current indexed view and returns Subgraph metadata. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with your Endpoint Alias. [Example project](/examples/subgraphs.guides.query-a-subgraph.sh-1.tar.gz) ```sh # GraphQL errors can arrive with HTTP 200: check the response for an "errors" array. curl --fail-with-body --silent --show-error "https://subgraphs.graphops.xyz/v1/alias/" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "query": "query TopPools($first: Int!) { pools(first: $first, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } }", "variables": { "first": 5 }, "operationName": "TopPools" }' ``` Output ```json { "data": { "pools": [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191967.4113592219436945119116405" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ], "_meta": { "block": { "number": 26285622, "hash": "0x0d79b8e3597d041e44c63645f9ea378a6fb856830bb663afc5e56eb26d3e72fa" }, "hasIndexingErrors": false } } } ``` Up to $first pools, largest first BigDecimal values arrive as strings The data reflects this block Treat true as a data-quality warning Prerequisites 1. Install Node.js. 2. Install tsx. `npm install tsx` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Node.js 24.22.1, tsx 4.23.15. [Example project](/examples/subgraphs.guides.query-a-subgraph.ts-2.tar.gz) ```typescript export {}; const response = await fetch( "https://subgraphs.graphops.xyz/v1/alias/", { method: "POST", headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: ` query TopPools($first: Int!) { pools( first: $first orderBy: totalValueLockedUSD orderDirection: desc ) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } `, variables: { first: 5 }, operationName: "TopPools", }), }, ); if (!response.ok) throw new Error(await response.text()); const { data, errors } = await response.json(); // GraphQL errors can arrive with HTTP 200, so check the body too. if (errors) throw new Error(JSON.stringify(errors)); console.log(JSON.stringify(data.pools)); // The indexed block these results were read from. console.log(JSON.stringify(data._meta.block.number)); ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191967.4113592219436945119116405" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] 26285622 ``` BigDecimal values arrive as strings Prerequisites 1. Install Go. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` with your Endpoint Alias. Tested with Go 1.26.0. [Example project](/examples/subgraphs.guides.query-a-subgraph.go-5.tar.gz) ```go package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { payload, err := json.Marshal(struct { Query string `json:"query"` Variables map[string]any `json:"variables"` OperationName string `json:"operationName"` }{ Query: ` query TopPools($first: Int!) { pools( first: $first orderBy: totalValueLockedUSD orderDirection: desc ) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } `, Variables: map[string]any{"first": 5}, OperationName: "TopPools", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPost, "https://subgraphs.graphops.xyz/v1/alias/", bytes.NewReader(payload)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { log.Fatalf("HTTP %d: %s", resp.StatusCode, body) } var result struct { Data struct { Pools json.RawMessage `json:"pools"` Meta struct { Block struct { Number json.RawMessage `json:"number"` } `json:"block"` } `json:"_meta"` } `json:"data"` Errors json.RawMessage `json:"errors"` } if err := json.Unmarshal(body, &result); err != nil { log.Fatal(err) } // GraphQL errors can arrive with HTTP 200, so check the body too. if result.Errors != nil { log.Fatalf("GraphQL errors: %s", result.Errors) } var pools bytes.Buffer if err := json.Compact(&pools, result.Data.Pools); err != nil { log.Fatal(err) } fmt.Println(pools.String()) // The indexed block these results were read from. fmt.Println(string(result.Data.Meta.Block.Number)) } ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191967.4113592219436945119116405" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] 26285622 ``` BigDecimal values arrive as strings Prerequisites 1. Install Rust. 2. Install reqwest, serde\_json, and tokio. `cargo add reqwest serde_json tokio -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Rust 1.98.1, reqwest 0.13.5, serde\_json 1.0.145, tokio 1.48.0. [Example project](/examples/subgraphs.guides.query-a-subgraph.rust-6.tar.gz) ```rust use serde_json::{json, Value}; use std::{env, error::Error, time::Duration}; #[tokio::main] async fn main() -> Result<(), Box> { let response = reqwest::Client::new() .post("https://subgraphs.graphops.xyz/v1/alias/") .bearer_auth(env::var("GRAPHOPS_API_KEY")?) .timeout(Duration::from_secs(30)) .json(&json!({ "query": r#" query TopPools($first: Int!) { pools( first: $first orderBy: totalValueLockedUSD orderDirection: desc ) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } "#, "variables": { "first": 5 }, "operationName": "TopPools", })) .send() .await? .error_for_status()?; let result: Value = response.json().await?; // GraphQL errors can arrive with HTTP 200, so check the body too. if let Some(errors) = result.get("errors") { return Err(errors.to_string().into()); } println!("{}", result["data"]["pools"]); // The indexed block these results were read from. println!("{}", result["data"]["_meta"]["block"]["number"]); Ok(()) } ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191967.4113592219436945119116405" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] 26285622 ``` BigDecimal values arrive as strings Prerequisites 1. Install Python. 2. Install requests. `python -m pip install requests` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your Endpoint Alias. Tested with Python 3.12.12, requests 2.34.2. [Example project](/examples/subgraphs.guides.query-a-subgraph.python-4.tar.gz) ```python import json import os import requests response = requests.post( "https://subgraphs.graphops.xyz/v1/alias/", headers={ "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, json={ "query": """ query TopPools($first: Int!) { pools( first: $first orderBy: totalValueLockedUSD orderDirection: desc ) { id token0 { symbol } token1 { symbol } totalValueLockedUSD } _meta { block { number hash } hasIndexingErrors } } """, "variables": {"first": 5}, "operationName": "TopPools", }, timeout=30, ) response.raise_for_status() result = response.json() # GraphQL errors can arrive with HTTP 200, so check the body too. if "errors" in result: raise RuntimeError(result["errors"]) print(json.dumps(result["data"]["pools"])) # The indexed block these results were read from. print(json.dumps(result["data"]["_meta"]["block"]["number"])) ``` Output ```json [ { "id": "0x277667eb3e34f134adf870be9550e9f323d0dc24", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-cvxsteCRV" }, "totalValueLockedUSD": "1110167010695.785900004139211677005" }, { "id": "0x8c0411f2ad5470a66cb2e9c64536cfb8dcd54d51", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-yvCurve-IronBank" }, "totalValueLockedUSD": "1432077483.208630196467118329099649" }, { "id": "0x055284a4ca6532ecc219ac06b577d540c686669d", "token0": { "symbol": "ease.org" }, "token1": { "symbol": "ez-SLP-WBTC-WETH" }, "totalValueLockedUSD": "1322031013.848042060988788389158996" }, { "id": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "385191967.4113592219436945119116405" }, { "id": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "token0": { "symbol": "USDC" }, "token1": { "symbol": "WETH" }, "totalValueLockedUSD": "321144439.2777974484977405304295032" } ] 26285622 ``` BigDecimal values arrive as strings ## Handle the result * Check for an `errors` array before you read `data`. GraphQL errors can arrive with HTTP `200`, and a response can contain both `data` and `errors`. * Treat an empty list as a valid result, not a failure. * Read `_meta.block.number` when you display or store the data, and alert when `_meta.hasIndexingErrors` is `true`. * Log the [`X-Deployment-CID` response header](../api#response-headers) with results you store, so you can tell which deployment produced them after an Endpoint Alias moves. * Request only the fields your application renders or stores, and page through larger collections. Use Endpoint Aliases for application endpoints when your team needs controlled upgrades. Use Deployment CIDs for tests and rollback targets that must stay on one immutable deployment. ## Next steps * [Paginate results](./paginate-results): Read a whole collection in bounded, resumable pages. * [Monitor sync health](./monitor-sync-health): Turn `_meta` into lag and error alerts. * [API integration](../api): Check the request envelope, endpoint shapes, and error responses. --- # Archive reads Source: https://docs.graphops.xyz/rpc/concepts/archive-reads Know when a request depends on historical data, so you can check coverage before you build on it and keep large historical jobs within timeouts and response limits. A request is an archive read when it asks for state, storage, code, balances, or event logs from older blocks. Archive coverage varies by network; check the Archive column in [Supported networks](../networks#supported-endpoints). When a network shows **Yes**, you can read historical state at past blocks, such as `eth_call` against an older block, and fetch historical event logs with `eth_getLogs`. For example, reading a USDC balance on Ethereum Mainnet at block `18000000` is an archive read; [Read contract state](../guides/read-contract-state) walks through that request. Archive requests are heavier than current-state reads. They can cost more credits and are more likely to reach response-size, timeout, or organization limits. ## Archive-sensitive methods | Method | Archive use | | ------------------ | ---------------------------------------------------------------- | | `eth_getLogs` | Historical event backfills over explicit block ranges. | | `eth_call` | Historical contract reads with an explicit block number or hash. | | `eth_getBalance` | Historical account balances. | | `eth_getStorageAt` | Historical storage-slot reads. | | `eth_getCode` | Historical contract bytecode reads. | | `eth_getProof` | Historical account and storage proof reads. | ## Range strategy _Reads of older blocks need archive data and are heavier than current-state reads. Backfill history in narrow, deterministic ranges and persist progress after every successful range, so a retry repeats only one range._ - Range 1: Stored - Range 2: Stored - Range 3: In flight - Range 4: Queued Split historical work into narrow, deterministic ranges instead of one unbounded `fromBlock` to `latest` scan: * Filter by contract address and topic whenever possible. * Widen ranges only after observing response size and latency. * Persist progress after every successful range. * Retry a smaller range after a timeout or response-size error. [Backfill historical events](../guides/backfill-historical-events) applies this model to a resumable USDC `Transfer` backfill. ## Next steps * [Backfill historical events](../guides/backfill-historical-events): Run a resumable log backfill with stored checkpoints. * [Supported networks](../networks#supported-endpoints): Check archive coverage for your network. * [Usage and limits](../usage-and-limits): See how method classes affect credit usage. --- # Consistency and finality Source: https://docs.graphops.xyz/rpc/concepts/consistency-and-finality A balance, price, and transaction count read at different blocks may not describe the same state. Pin related calls to one block so your application compares like with like. Choose finality based on whether the result can tolerate a later chain reorganization. ## Block references | Reference | Use it when | | ------------ | ---------------------------------------------------------------------------------------------- | | `latest` | You want the freshest available view and can tolerate best-effort consistency across requests. | | `safe` | You want a safer recent block for user-visible reads. | | `finalized` | You want finalized data and can tolerate additional latency. | | Block number | You need repeatable reads across multiple requests. | | Block hash | You need to anchor work to one exact block. | _The latest block is the newest. The safe block trails it and is unlikely to be reorganized, and the finalized block will not change. Distances between them vary by network, so pin related reads to one block number or hash._ - `finalized`: block N-5 in this example. - `safe`: block N-3 in this example. - `latest`: block N in this example. ## Repeatable read pattern When multiple calls must observe the same state: 1. Select a block with `eth_blockNumber`, or with `eth_getBlockByNumber` and a tag such as `safe` or `finalized`. 2. Store the block number or block hash. 3. Pass that same reference to follow-up `eth_call`, `eth_getLogs`, `eth_getBalance`, `eth_getStorageAt`, or `eth_getCode` requests. For example, these two reads can describe different blocks, because each `latest` resolves when its request arrives: ```json [ { "method": "eth_getBalance", "params": ["0x...", "latest"] }, { "method": "eth_call", "params": [{ "to": "0x...", "data": "0x..." }, "latest"] } ] ``` These two reads describe the same block, `18000000`: ```json [ { "method": "eth_getBalance", "params": ["0x...", "0x112a880"] }, { "method": "eth_call", "params": [{ "to": "0x...", "data": "0x..." }, "0x112a880"] } ] ``` Sending the reads in one batch does not pin them; only the explicit block reference does. [Read contract state](../guides/read-contract-state#read-several-values-at-one-block) shows the pattern in client code. ## Choose finality A pinned block can still be reorganized if it is newer than `finalized`. Pick the reference by what a reorganization would cost you: * Use `latest` for displays that refresh and can show a brief inconsistency. * Use `safe` for user-visible reads that should rarely change. * Use `finalized` for settlement, accounting, or any irreversible step. When you store data from recent blocks, keep enough block identity to undo it. [Retries and reorgs](../guides/retries-and-reorgs#handle-reorgs) shows how. ## Next steps * [Read contract state](../guides/read-contract-state): Pin several `eth_call` reads to one finalized block. * [Retries and reorgs](../guides/retries-and-reorgs): Undo data from replaced blocks and retry on the same block. * [Supported networks](../networks): Check network notes for L2 finality differences. --- # Backfill historical events Source: https://docs.graphops.xyz/rpc/guides/backfill-historical-events Build a transfer history, audit trail, or analytics table from months of contract events, and resume it after any failure without starting over. This guide scans USDC `Transfer` logs in bounded block ranges and stores a checkpoint after each range. Older ranges need archive access; check [Supported networks](../networks#supported-endpoints) for coverage on your network. ## Prerequisites * An RPC API key. * Archive access for the target chain. * A durable store for logs and range progress. * An idempotency key such as `(blockHash, transactionHash, logIndex)`. ## How the backfill runs *Ranges 1 and 2 are stored and the checkpoint records block 18,003,839. Range 3 is in flight; after its logs are written, the checkpoint moves to 18,005,759. A restart resumes from the checkpoint and repeats at most one range.* - Range 1: blocks 18,000,000 to 18,001,919. Stored. - Range 2: blocks 18,001,920 to 18,003,839. Stored. - Range 3: blocks 18,003,840 to 18,005,759. In flight. - Range 4: blocks 18,005,760 to 18,007,679. Queued. - Checkpoint: `lastCompletedBlock: 18003839`. - Next request: `eth_getLogs` with `"fromBlock": "0x112b780"` and `"toBlock": "0x112beff"`. 1. Choose a starting block and a range size. This guide uses 1,920 blocks per range. 2. Request one range with `eth_getLogs`, filtered by contract address and event topic. 3. Write the returned logs idempotently, keyed by `(blockHash, transactionHash, logIndex)`. 4. Store the range's last block as your checkpoint, then request the next range. Run one live consumer separately for new blocks, such as a [WebSocket subscription](./websocket-subscriptions), and stop the backfill where the live consumer starts. ## Request Each variant requests the first range, blocks 18,000,000 to 18,001,919. Repeat the same request shape for each following range. Prerequisites 1. Install jq. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with jq 1.8.2. [Example project](/examples/rpc.guides.backfill-historical-events.sh-1.tar.gz) ```sh # The first 1,920-block range: 0x112a880-0x112afff is blocks 18,000,000-18,001,919. # The topic is keccak256("Transfer(address,address,uint256)"), the Transfer event signature. curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getLogs", "params": [ { "fromBlock": "0x112a880", "toBlock": "0x112afff", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ] } ] }' | jq '{logCount: (.result | length)}' ``` Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.backfill-historical-events.ts-2.tar.gz) ```typescript import { createPublicClient, http, parseAbiItem } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); // The first 1,920-block range. Request each following range the same way. const logs = await client.getLogs({ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 value)", ), fromBlock: 18_000_000n, toBlock: 18_001_919n, }); console.log(JSON.stringify({ logCount: logs.length })); // Store logs idempotently, then save toBlock as the checkpoint in your durable store. ``` Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.guides.backfill-historical-events.go-6.tar.gz) ```go package main import ( "context" "encoding/json" "fmt" "log" "math/big" "os" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() // The first 1,920-block range. Request each following range the same way. logs, err := client.FilterLogs(ctx, ethereum.FilterQuery{ FromBlock: big.NewInt(18_000_000), ToBlock: big.NewInt(18_001_919), Addresses: []common.Address{common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")}, // keccak256("Transfer(address,address,uint256)"), the Transfer event topic. Topics: [][]common.Hash{{ common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), }}, }) if err != nil { log.Fatal(err) } summary, err := json.Marshal(map[string]int{"logCount": len(logs)}) if err != nil { log.Fatal(err) } fmt.Println(string(summary)) // Store logs idempotently, then save toBlock as the checkpoint in your durable store. } ``` Prerequisites 1. Install Rust. 2. Install alloy, reqwest, tokio, and serde\_json. `cargo add alloy reqwest tokio serde_json -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0, serde\_json 1.0.145. [Example project](/examples/rpc.guides.backfill-historical-events.rust-7.tar.gz) ```rust use alloy::primitives::address; use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::client::RpcClient; use alloy::rpc::types::Filter; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use serde_json::json; use std::{env, error::Error}; #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); // The first 1,920-block range. Request each following range the same way. let filter = Filter::new() .address(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")) .event("Transfer(address,address,uint256)") .from_block(18_000_000) .to_block(18_001_919); let logs = provider.get_logs(&filter).await?; println!("{}", json!({ "logCount": logs.len() })); // Store logs idempotently, then save to_block as the checkpoint in your durable store. Ok(()) } ``` Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.guides.backfill-historical-events.python-4.tar.gz) ```python import json import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) # The first 1,920-block range. Request each following range the same way. logs = w3.eth.get_logs( { "fromBlock": 18_000_000, "toBlock": 18_001_919, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # keccak256("Transfer(address,address,uint256)"), the Transfer event topic. "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ], } ) print(json.dumps({"logCount": len(logs)})) # Store logs idempotently, then save toBlock as the checkpoint in your durable store. ``` ## Range sizing Start with small ranges and widen them only after observing response size and latency. Avoid unbounded `fromBlock` to `latest` scans; a failed broad request repeats all of its work. | Situation | Action | | ------------------ | ----------------------------------------------- | | Timeout | Reduce range size. | | Response too large | Reduce range size or add more selective topics. | | Sparse logs | Increase range size gradually. | | Rate limit (`429`) | Lower concurrency and add backoff with jitter. | ## Progress tracking Store the checkpoint in the same transaction as the range's logs, so a restart never skips a range. After the first range, the record looks like this: ```json { "network": "ethereum", "chainId": 1, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "lastCompletedBlock": 18001919 } ``` On restart, read `lastCompletedBlock` and request the next range from the block after it. ## Next steps * [Archive reads](../concepts/archive-reads): Check which methods and ranges need archive access. * [Usage and limits](../usage-and-limits): Size worker concurrency within your organization's shared limits. * [WebSocket subscriptions](./websocket-subscriptions): Keep the history current with live events once the backfill catches up. --- # Fetch logs Source: https://docs.graphops.xyz/rpc/guides/fetch-logs Build a transfer history, audit trail, or analytics table from contract events. This guide reads the USDC `Transfer` logs emitted in Ethereum mainnet blocks `18000002` to `18000007`, then shows how to keep larger log scans bounded and safe to resume. ## Prerequisites * An RPC API key. * Ethereum mainnet endpoint access. * An application path for storing or processing returned logs. ## Request This request reads the USDC `Transfer` logs in blocks `18000002` to `18000007`, filtered by contract and topic. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.guides.fetch-logs.sh-1.tar.gz) ```sh # Block numbers are hex quantities: 0x112a882-0x112a887 is blocks 18,000,002-18,000,007. # The topic is keccak256("Transfer(address,address,uint256)"), the Transfer event signature. curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getLogs", "params": [ { "fromBlock": "0x112a882", "toBlock": "0x112a887", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ] } ] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "blockHash": "0x5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "blockNumber": "0x112a882", "blockTimestamp": "0x64ea26a7", "transactionHash": "0x29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d", "transactionIndex": "0x2b", "logIndex": "0xac", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "blockHash": "0x5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "blockNumber": "0x112a882", "blockTimestamp": "0x64ea26a7", "transactionHash": "0xa8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db", "transactionIndex": "0x71", "logIndex": "0x13b", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000180cd4303582d348a54fcb0436d5665f082a23d1", "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43" ], "data": "0x00000000000000000000000000000000000000000000000000000000aad0a340", "blockHash": "0x90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4", "blockNumber": "0x112a883", "blockTimestamp": "0x64ea26b3", "transactionHash": "0x05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3", "transactionIndex": "0x66", "logIndex": "0xde", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000001c727a55ea3c11b0ab7d3a361fe0f3c47ce6de5d", "0x000000000000000000000000c3dcc088aef4373e665b00fc8be45a4b6a4b24a6" ], "data": "0x000000000000000000000000000000000000000000000000000000000686bcda", "blockHash": "0x90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4", "blockNumber": "0x112a883", "blockTimestamp": "0x64ea26b3", "transactionHash": "0xeb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42", "transactionIndex": "0x72", "logIndex": "0xe6", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000043954785980c50768373dc1dafd7ba3a2052036f" ], "data": "0x000000000000000000000000000000000000000000000000000000e8d433aa6e", "blockHash": "0x7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2", "blockNumber": "0x112a884", "blockTimestamp": "0x64ea26bf", "transactionHash": "0xb2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690", "transactionIndex": "0x48", "logIndex": "0xbf", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c880d8be4e94f644f1daa7d3558b50ff5a1c2db7", "0x000000000000000000000000ae2d4617c862309a3d75a0ffb358c7a5009c673f" ], "data": "0x0000000000000000000000000000000000000000000000000000002e90edd000", "blockHash": "0x127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7", "blockNumber": "0x112a885", "blockTimestamp": "0x64ea26cb", "transactionHash": "0x1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d", "transactionIndex": "0x1e", "logIndex": "0x40", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000b86fc5e985facdcae6034beefc6cbdcd8bbd8cb4", "0x00000000000000000000000027f713adefc708469d826ab8f5b1e4672f1a070e" ], "data": "0x0000000000000000000000000000000000000000000000000000000059682f00", "blockHash": "0x127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7", "blockNumber": "0x112a885", "blockTimestamp": "0x64ea26cb", "transactionHash": "0x440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c", "transactionIndex": "0x6d", "logIndex": "0xcb", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000b4eae88d1643e172518dde0ef213c20628798c65", "0x00000000000000000000000038c5a4f22e2e290ba9bbb9e3d9aaf8582f79f4d1" ], "data": "0x0000000000000000000000000000000000000000000000000000000006b78840", "blockHash": "0xdb63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1", "blockNumber": "0x112a887", "blockTimestamp": "0x64ea26e3", "transactionHash": "0x21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea", "transactionIndex": "0x1f", "logIndex": "0x5e", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000579752cff8fee7af09446b2133ee2f9ff10c4fbf", "0x0000000000000000000000001715a3e4a142d8b698131108995174f37aeba10d" ], "data": "0x000000000000000000000000000000000000000000000000000000001d0221cd", "blockHash": "0xdb63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1", "blockNumber": "0x112a887", "blockTimestamp": "0x64ea26e3", "transactionHash": "0xd78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c", "transactionIndex": "0x57", "logIndex": "0xb3", "removed": false } ] } ``` Every matching log in the requested block range USDC contract Transfer event signature Transfer amount: 1,075,000,000 base units, 1,075 USDC Block in decimal: 18,000,002 Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.fetch-logs.ts-2.tar.gz) ```typescript import { createPublicClient, http, parseAbiItem } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const logs = await client.getLogs({ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 value)", ), fromBlock: 18_000_002n, toBlock: 18_000_007n, }); console.log(logs.length); ``` Output ```json 9 ``` Matching Transfer logs in the requested block: 9 Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.guides.fetch-logs.go-5.tar.gz) ```go package main import ( "context" "fmt" "log" "math/big" "os" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() logs, err := client.FilterLogs(ctx, ethereum.FilterQuery{ FromBlock: big.NewInt(18_000_002), ToBlock: big.NewInt(18_000_007), Addresses: []common.Address{common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")}, // keccak256("Transfer(address,address,uint256)"), the Transfer event topic. Topics: [][]common.Hash{{ common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), }}, }) if err != nil { log.Fatal(err) } fmt.Println(len(logs)) } ``` Output ```json 9 ``` Matching Transfer logs in the requested block: 9 Prerequisites 1. Install Rust. 2. Install alloy, reqwest, and tokio. `cargo add alloy reqwest tokio -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0. [Example project](/examples/rpc.guides.fetch-logs.rust-6.tar.gz) ```rust use alloy::primitives::address; use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::client::RpcClient; use alloy::rpc::types::Filter; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use std::{env, error::Error}; #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); let filter = Filter::new() .address(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")) .event("Transfer(address,address,uint256)") .from_block(18_000_002) .to_block(18_000_007); let logs = provider.get_logs(&filter).await?; println!("{}", logs.len()); Ok(()) } ``` Output ```json 9 ``` Matching Transfer logs in the requested block: 9 Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.guides.fetch-logs.python-4.tar.gz) ```python import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) logs = w3.eth.get_logs( { "fromBlock": 18_000_002, "toBlock": 18_000_007, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # keccak256("Transfer(address,address,uint256)"), the Transfer event topic. "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ], } ) print(len(logs)) ``` Output ```json 9 ``` Matching Transfer logs in the requested block: 9 ## Read the output The range has nine USDC `Transfer` logs. The CLI tab prints them with their fields annotated; the TypeScript, Go, Rust, and Python tabs print the count. ## Before production * Always bound historical `eth_getLogs` requests by block range. * Filter by contract address and topic when possible. * Store the last completed block range for backfills. * Deduplicate logs by `(blockHash, transactionHash, logIndex)`. * If a range times out or hits a response-size limit, retry smaller ranges. ## Next steps * [Backfill historical events](./backfill-historical-events): Scan a long block range in bounded, resumable chunks. * [Supported methods](../methods#logs-and-filters): Check `eth_getLogs` parameters and transport support. * [Archive reads](../concepts/archive-reads): Know which ranges need archive access on your network. --- # Read contract state Source: https://docs.graphops.xyz/rpc/guides/read-contract-state Read a token balance, pool reserve, or any other view function without sending a transaction. This guide reads the USDC balance of the Uniswap V2 USDC/WETH pair at block `18000000` with `eth_call`, then pins several reads to one block so their results describe the same chain state. ## Prerequisites * An RPC API key. * Ethereum mainnet endpoint access. * A contract address, ABI or encoded call data, and a block reference. ## Request The `data` field is `balanceOf(address)` encoded with selector `0x70a08231` and the target address left-padded to 32 bytes. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.guides.read-contract-state.sh-1.tar.gz) ```sh # data is the balanceOf(address) selector 0x70a08231 followed by the holder address # left-padded to 32 bytes. 0x112a880 is block 18,000,000. curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_call", "params": [ { "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "data": "0x70a08231000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" }, "0x112a880" ] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x0000000000000000000000000000000000000000000000000000197016bc99ff" } ``` Matches the id you sent Token amount: 27,969,208,490,495 base units, 27,969,208.490495 USDC Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.read-contract-state.ts-2.tar.gz) ```typescript import { createPublicClient, erc20Abi, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const balance = await client.readContract({ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi: erc20Abi, functionName: "balanceOf", args: ["0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"], blockNumber: 18_000_000n, }); console.log(balance.toString()); ``` Output ```json 27969208490495 ``` Token amount: 27,969,208,490,495 base units, 27,969,208.490495 USDC Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.guides.read-contract-state.go-6.tar.gz) ```go package main import ( "context" "fmt" "log" "math/big" "os" "strings" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) const balanceOfABI = `[{ "name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "owner", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}] }]` func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() usdcABI, err := abi.JSON(strings.NewReader(balanceOfABI)) if err != nil { log.Fatal(err) } data, err := usdcABI.Pack("balanceOf", common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")) if err != nil { log.Fatal(err) } usdc := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") output, err := client.CallContract(ctx, ethereum.CallMsg{To: &usdc, Data: data}, big.NewInt(18_000_000)) if err != nil { log.Fatal(err) } var balance *big.Int if err := usdcABI.UnpackIntoInterface(&balance, "balanceOf", output); err != nil { log.Fatal(err) } fmt.Println(balance) } ``` Output ```json 27969208490495 ``` Token amount: 27,969,208,490,495 base units, 27,969,208.490495 USDC Prerequisites 1. Install Rust. 2. Install alloy, reqwest, and tokio. `cargo add alloy reqwest tokio -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0. [Example project](/examples/rpc.guides.read-contract-state.rust-7.tar.gz) ```rust use alloy::primitives::address; use alloy::providers::ProviderBuilder; use alloy::rpc::client::RpcClient; use alloy::sol; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use std::{env, error::Error}; sol! { #[sol(rpc)] interface IERC20 { function balanceOf(address owner) external view returns (uint256); } } #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); let usdc = IERC20::new( address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), &provider, ); let balance = usdc .balanceOf(address!("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")) .block(18_000_000.into()) .call() .await?; println!("{balance}"); Ok(()) } ``` Output ```json 27969208490495 ``` Token amount: 27,969,208,490,495 base units, 27,969,208.490495 USDC Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.guides.read-contract-state.python-4.tar.gz) ```python import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) usdc = w3.eth.contract( address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi=[ { "name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "owner", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}], } ], ) balance = usdc.functions.balanceOf( "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" ).call(block_identifier=18_000_000) print(balance) ``` Output ```json 27969208490495 ``` Token amount: 27,969,208,490,495 base units, 27,969,208.490495 USDC ## Read the output USDC has 6 decimals, so divide the decoded integer by 1,000,000. Client libraries such as `viem`, `ethclient`, `alloy`, and `web3.py` decode the bytes for you and return the integer. ## Read several values at one block Separate `latest` reads can land on different blocks as the chain advances. When results must agree, such as two pool balances used to compute a price, resolve one block first and pass its number to every read. *Resolve one block, then pass its number to every read. Both balances describe block N even after latest moves on, so the pool price you compute from them is consistent.* 1. `eth_getBlockByNumber("finalized")`: returns block N. 2. `eth_call` USDC.balanceOf(pair) at block N. 3. `eth_call` WETH.balanceOf(pair) at block N. This `viem` example resolves the finalized block, then reads the pair's USDC and WETH balances at that block: Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.read-contract-state.ts-5.tar.gz) ```typescript import { type Address, createPublicClient, erc20Abi, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const pair = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"; // Pin both reads to one finalized block so the balances are consistent. const block = await client.getBlock({ blockTag: "finalized" }); const balanceOf = (token: Address) => client.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [pair], blockNumber: block.number, }); const [usdc, weth] = await Promise.all([ balanceOf("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), balanceOf("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), ]); console.log( JSON.stringify({ block: block.number.toString(), usdc: usdc.toString(), weth: weth.toString(), }), ); ``` Output ```json { "block": "26285536", "usdc": "9800805397315", "weth": "4175526930203265664930" } ``` Block in decimal: 26,285,536 USDC balance: 9,800,805,397,315 base units, 9,800,805.397315 USDC WETH balance: 4,175,526,930,203,265,664,930 base units, 4,175.52693020326566493 WETH Use `finalized` when the result must not change after a reorganization, `safe` for user-visible reads that can trail the head slightly, or a block number you stored earlier to repeat a read. See [Consistency and finality](../concepts/consistency-and-finality) for how the tags differ. ## Interpret errors * A revert is a contract execution result, not a transport failure. The JSON-RPC `error` object carries the revert data when the contract provides it, and retrying the same call at the same block reverts again. * Retry only when the HTTP status or JSON-RPC error indicates a transport or server failure. See [Retries and reorgs](./retries-and-reorgs). * A historical block may need archive access on your network. See [Archive reads](../concepts/archive-reads). ## Next steps * [Supported methods](../methods#state-and-execution): Check parameters for `eth_call`, `eth_getBalance`, and `eth_getStorageAt`. * [Consistency and finality](../concepts/consistency-and-finality): Choose between `latest`, `safe`, `finalized`, and a stored block. * [Fetch logs](./fetch-logs): Read the events a contract emitted instead of its current state. --- # Retries and reorgs Source: https://docs.graphops.xyz/rpc/guides/retries-and-reorgs Recover from temporary failures without duplicating work, and keep stored data correct when the chain reorganizes. Match each retry to the kind of request, and store enough block identity to undo data from replaced blocks. ## Prerequisites * A retry budget for your request path. * Logging for method, network, block reference, HTTP status, and JSON-RPC error code. * A durable cursor for long-running log or WebSocket workflows. ## Retry guidance | Request type | Retry guidance | | ------------------------ | ----------------------------------------------------------------------- | | Current-state reads | Retry `429`, `502`, `503`, and `504` with backoff and jitter. | | Historical reads | Retry smaller ranges when a request times out or returns too much data. | | `eth_call` simulations | Retry transport failures; treat EVM reverts as application results. | | `eth_sendRawTransaction` | Retry the same signed transaction; do not mutate and resubmit blindly. | | Batch requests | Retry failed items individually. | | WebSocket subscriptions | Reconnect, resubscribe, and backfill missed blocks. | For every retry: * Use exponential backoff with jitter, and cap the total retry time. * Log the method, network, block reference or range, HTTP status, and JSON-RPC error code. * Pass the same explicit block number or hash when the retry must observe the same state as the first attempt. A retried `latest` read can land on a newer block. See [API integration](../api#error-responses) for the meaning of each status and error code. ## Handle reorgs Recent blocks can be replaced by a chain reorganization. Logs you already processed from those blocks no longer describe the chain, and the replacement blocks can contain different logs. *A reorg replaces blocks N+1 and N+2. A logs subscription sends the replaced logs again with removed: true, then the logs from the new blocks. Keying logs by block hash keeps both versions apart; finalized blocks are not reorganized.* - Before: N-1, N, N+1, N+2. - After: N-1, N, N+1′, N+2′. - Logs from N+1, N+2 with `"removed": true`: delete each by (blockHash, transactionHash, logIndex). - Logs from N+1′, N+2′ with `"removed": false`: insert; the new blockHash keeps their keys distinct. 1. Key stored logs by `(blockHash, transactionHash, logIndex)`. The block hash keeps logs from a replaced block distinct from logs in its replacement. 2. Over a WebSocket `logs` subscription, treat each log with `removed: true` as a reversal and delete or undo the matching stored log. 3. When you poll with `eth_getLogs`, keep the block hashes of recent blocks you processed. Compare them with `eth_getBlockByNumber` on each poll, and replay from the last block whose hash still matches. 4. For settlement, accounting, or other irreversible steps, act only on data at or behind the `finalized` block. Use `safe` when you can accept a small reorg risk for lower latency. ## Next steps * [Consistency and finality](../concepts/consistency-and-finality): Choose block references that keep related reads on one state. * [WebSocket subscriptions](./websocket-subscriptions): Recover a live stream after a disconnect. * [Usage and limits](../usage-and-limits): Plan retry volume within your organization's shared limits. --- # Send a raw transaction Source: https://docs.graphops.xyz/rpc/guides/send-raw-transaction Submit a payment, swap, or contract call that you signed in your wallet, signer service, or application. GraphOps RPC broadcasts the signed payload with `eth_sendRawTransaction` and does not custody private keys. You store the returned hash, poll for the receipt, and decide when the transaction is final enough to report success. ## Prerequisites * An RPC API key. * Ethereum mainnet endpoint access. * A locally signed transaction for the same chain ID as the endpoint. * Application-side nonce, gas, and fee handling. ## Prepare the transaction Before signing, check the chain and plan fees against the same endpoint you will submit to: 1. Call `eth_chainId` and confirm it matches the chain ID in the transaction. This prevents a wrong-chain submission. 2. Estimate gas with `eth_estimateGas`, and set fees from `eth_feeHistory` and `eth_maxPriorityFeePerGas`. 3. Sign the transaction locally and keep the signed bytes until the receipt confirms it. ## Request Replace `0x` with your signed bytes. The recording comes from a local Anvil test chain. Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with your signed, hex-encoded transaction. [Example project](/examples/rpc.guides.send-raw-transaction.sh-1.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": ["0x"] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5" } ``` Transaction hash returned by the submitted operation Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your signed, hex-encoded transaction. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.send-raw-transaction.ts-2.tar.gz) ```typescript import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://rpc.graphops.xyz/v1/evm/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`, }, }, }), }); const hash = await client.request({ method: "eth_sendRawTransaction", params: ["0x"], }); console.log(JSON.stringify(hash)); ``` Output ```json "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5" ``` Transaction hash returned by the submitted operation Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your signed, hex-encoded transaction. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.guides.send-raw-transaction.go-7.tar.gz) ```go package main import ( "context" "encoding/json" "fmt" "log" "os" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) func main() { ctx := context.Background() rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1", rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } client := ethclient.NewClient(rpcClient) defer client.Close() raw, err := hexutil.Decode("0x") if err != nil { log.Fatal(err) } tx := new(types.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { log.Fatal(err) } if err := client.SendTransaction(ctx, tx); err != nil { log.Fatal(err) } txHash, err := json.Marshal(tx.Hash()) if err != nil { log.Fatal(err) } fmt.Println(string(txHash)) } ``` Output ```json "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5" ``` Transaction hash returned by the submitted operation Prerequisites 1. Install Rust. 2. Install alloy, reqwest, tokio, and serde\_json. `cargo add alloy reqwest tokio serde_json -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your signed, hex-encoded transaction. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0, serde\_json 1.0.145. [Example project](/examples/rpc.guides.send-raw-transaction.rust-8.tar.gz) ```rust use alloy::hex; use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::client::RpcClient; use alloy::transports::http::Http; use reqwest::header::{HeaderMap, AUTHORIZATION}; use std::{env, error::Error}; #[tokio::main] async fn main() -> Result<(), Box> { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?, ); let http = reqwest::Client::builder() .default_headers(headers) .build()?; let provider = ProviderBuilder::new().connect_client(RpcClient::new( Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?), false, )); let signed = hex::decode("0x")?; let pending = provider.send_raw_transaction(&signed).await?; println!("{}", serde_json::to_string(pending.tx_hash())?); Ok(()) } ``` Output ```json "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5" ``` Transaction hash returned by the submitted operation Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 4. Replace `` with your signed, hex-encoded transaction. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.guides.send-raw-transaction.python-4.tar.gz) ```python import json import os from web3 import Web3 w3 = Web3( Web3.HTTPProvider( "https://rpc.graphops.xyz/v1/evm/1", request_kwargs={ "headers": { "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}", }, }, ) ) tx_hash = w3.eth.send_raw_transaction("0x") print(json.dumps(tx_hash.to_0x_hex())) ``` Output ```json "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5" ``` Transaction hash returned by the submitted operation ## Store the hash The hash identifies the transaction before it is included in a block. Store it before you report progress to your application, so a crash or timeout can resume from the hash instead of signing again. ## Wait for the receipt *The hash comes back at submission, before the transaction is in a block. The receipt is null until inclusion, then shows the result in block N. Your confirmation policy decides when block N is final enough to report success.* 1. `eth_sendRawTransaction` returns `0x8a…17`: store the hash first. 2. `eth_getTransactionReceipt` returns `null`: not included yet: poll again. 3. `eth_getTransactionReceipt` returns `"status": "0x1"`: included in block N. 4. Wait until block N reaches safe or finalized: your policy. Poll `eth_getTransactionReceipt` with the stored hash. It returns `null` until the transaction is included in a block: Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 2. Replace `` with the transaction hash returned when you sent it. [Example project](/examples/rpc.guides.send-raw-transaction.sh-6.tar.gz) ```sh curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt", "params": ["0x"] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": { "type": "0x0", "status": "0x1", "cumulativeGasUsed": "0x5208", "logs": [], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "transactionHash": "0x536129e5bd563374f67d3131b7edb31cd9b95eaa89526b23f3de05ca11592dc5", "transactionIndex": "0x0", "blockHash": "0x48ba51e86cbc56519a4303a92eaa607a985abddfe40a0171b8c7b37e0e5a8459", "blockNumber": "0x1", "gasUsed": "0x5208", "effectiveGasPrice": "0x3b9aca00", "blobGasPrice": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "contractAddress": null } } ``` 0x1 succeeded; 0x0 reverted but still paid gas The hash you stored Recheck this if the block reorganizes The block that included it Gas cost is gasUsed times this A receipt shows inclusion, not finality. For payments, settlement, or other irreversible steps, wait until the receipt's block is at or behind the `safe` or `finalized` block your workflow requires. If the block is reorganized, the transaction can return to the pending pool or be included in a different block, so check the receipt again before acting on it. ## Retry guidance Retrying the exact same signed transaction is usually safe because the transaction hash is deterministic. Do not mutate gas, nonce, or value and retry blindly. | Message | Meaning | Action | | ------------------------------------- | --------------------------------------------- | ---------------------------------------------------- | | `already known` | A node already has the transaction. | Track the transaction hash and poll for a receipt. | | `nonce too low` | The nonce has already been used. | Check whether the transaction was mined or replaced. | | `replacement transaction underpriced` | A replacement transaction did not pay enough. | Reprice according to wallet policy. | | `insufficient funds` | Sender cannot cover value plus gas. | Surface the error to the funding workflow. | ## Next steps * [Retries and reorgs](./retries-and-reorgs): Decide which failures to retry and how to recover from a reorganization. * [Supported methods](../methods#transactions): Check parameters and results for transaction and receipt methods. * [API integration](../api#error-responses): Interpret JSON-RPC error codes and HTTP statuses. --- # WebSocket subscriptions Source: https://docs.graphops.xyz/rpc/guides/websocket-subscriptions Update balances, send notifications, or start jobs as new blocks and matching events arrive. This guide subscribes to USDC `Transfer` logs over WebSocket, then shows how to fill the gap with `eth_getLogs` when the connection drops, because a new subscription only delivers new events. ## Prerequisites * An RPC API key. * WebSocket access for the target chain. * A WebSocket client. Use route-token authentication when the client cannot send custom headers. * Durable progress tracking for the last processed block and log identity. ## Subscribe to logs This request subscribes to USDC `Transfer` logs over the Ethereum mainnet WebSocket endpoint. The endpoint uses route-token authentication, so the URL contains your API key; redact it from logs, metrics labels, and support output. Prerequisites 1. Install Node.js. 2. Install tsx and ws. `npm install tsx ws` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, ws 8.22.0. [Example project](/examples/rpc.guides.websocket-subscriptions.ts-1.tar.gz) ```typescript import WebSocket from "ws"; const socket = new WebSocket( `wss://rpc.graphops.xyz/v1/evm/1/${process.env.GRAPHOPS_API_KEY}`, ); socket.once("error", (error) => { throw error; }); socket.once("open", () => { socket.send( JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_subscribe", params: [ "logs", { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // keccak256("Transfer(address,address,uint256)"), the Transfer event topic. topics: [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", ], }, ], }), ); }); let done = false; // This demonstration prints one notification. socket.on("message", (data) => { const message = JSON.parse(data.toString()); if (message.error) throw new Error(JSON.stringify(message.error)); // The first reply carries the subscription id; each log then arrives as an // eth_subscription notification. if (message.method !== "eth_subscription" || done) return; done = true; console.log(JSON.stringify(message)); socket.close(); }); ``` Output ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "result": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x00000000000000000000000093228d328c9c74c2bfe9f97638bbb5ef322f2bd5", "0x00000000000000000000000021a1adfc2b88b1479f1307817e900addbc15b3f2" ], "data": "0x000000000000000000000000000000000000000000000000000000002f237ebc", "blockHash": "0x083457ed25b83ce5f2ee9fa527e4f08a9ea7b98cceb6b7cccb23383d5b2f5abe", "blockNumber": "0x1911630", "blockTimestamp": "0x6ae0e297", "transactionHash": "0x104106af1f15a9bfaf0e0840626b316dcbfc6f89f557942170d031943d8c184c", "transactionIndex": "0x8", "logIndex": "0x4", "removed": false }, "subscription": "0xfbcbd5f54eff400f111f11b1c34cedee" } } ``` Transfer amount: 790,855,356 base units, 790.855356 USDC Block in decimal: 26,285,616 Subscription ID returned by eth\_subscribe The program prints one notification, then closes the subscription. The subscription ID in `params.subscription` identifies the subscription; notifications carry no request `id`. ## Use a client library Client libraries manage the subscription for you over the route-token WebSocket endpoint: viem with `watchEvent`, go-ethereum with `SubscribeFilterLogs`, alloy with `subscribe_logs`, and web3.py with its asynchronous WebSocket provider. Prerequisites 1. Install Node.js. 2. Install tsx and viem. `npm install tsx viem` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10. [Example project](/examples/rpc.guides.websocket-subscriptions.ts-4.tar.gz) ```typescript import { createPublicClient, parseAbiItem, webSocket } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: webSocket(`wss://rpc.graphops.xyz/v1/evm/1/${process.env.GRAPHOPS_API_KEY}`), }); let done = false; // This demonstration stops after the first log. const unwatch = client.watchEvent({ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", event: parseAbiItem( "event Transfer(address indexed from, address indexed to, uint256 value)", ), // Over WebSocket, viem delivers each eth_subscription log as a one-item array. onLogs: async (logs) => { if (done) return; done = true; console.log( JSON.stringify(logs, (_, value) => typeof value === "bigint" ? value.toString() : value, ), ); unwatch(); // Close the socket so the process can exit. const socket = await client.transport.getRpcClient(); socket.close(); }, onError: (error) => { throw error; }, }); ``` Output ```json [ { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "0x00000000000000000000000023617e59a5925b2a4bf75d73ff6711cd0b29de85" ], "data": "0x0000000000000000000000000000000000000000000000000000003114368392", "blockHash": "0x37db6e4e06418099e0dde9c71a1330ceb05843f1d79ef4440a27498822c6a03e", "blockNumber": "26285617", "blockTimestamp": "1793122979", "transactionHash": "0x205c7d8e77b18b559b10351720edf1136bb23932cfc27f646d3848730d3de77a", "transactionIndex": 0, "logIndex": 10, "removed": false, "args": { "from": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", "to": "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", "value": "210792514450" }, "eventName": "Transfer" } ] ``` Store as your last processed block true when a reorg reverses this log Prerequisites 1. Install Go. 2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7. [Example project](/examples/rpc.guides.websocket-subscriptions.go-7.tar.gz) ```go package main import ( "context" "encoding/json" "fmt" "log" "os" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) func main() { ctx := context.Background() client, err := ethclient.DialContext(ctx, fmt.Sprintf("wss://rpc.graphops.xyz/v1/evm/1/%s", os.Getenv("GRAPHOPS_API_KEY"))) if err != nil { log.Fatal(err) } defer client.Close() logs := make(chan types.Log) sub, err := client.SubscribeFilterLogs(ctx, ethereum.FilterQuery{ Addresses: []common.Address{common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")}, // keccak256("Transfer(address,address,uint256)"), the Transfer event topic. Topics: [][]common.Hash{{ common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), }}, }, logs) if err != nil { log.Fatal(err) } defer sub.Unsubscribe() // This demonstration stops after the first log. select { case err := <-sub.Err(): log.Fatal(err) case transfer := <-logs: payload, err := json.Marshal(transfer) if err != nil { log.Fatal(err) } fmt.Println(string(payload)) } } ``` Output ```json { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90", "0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000055", "blockNumber": "0x1911634", "transactionHash": "0x078a0450b9831ce36fe6b7aed8b4cd34a5e8a5da626d1fbf8be2efa3863e867f", "transactionIndex": "0x0", "blockHash": "0x7931a9fc337c615e67690a3083685a5680a72653718c13451f7697ea856b14f3", "blockTimestamp": "0x6ae0e2c7", "logIndex": "0x0", "removed": false } ``` Store as your last processed block true when a reorg reverses this log Prerequisites 1. Install Rust. 2. Install alloy, reqwest, tokio, and serde\_json. `cargo add alloy reqwest tokio serde_json -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0, serde\_json 1.0.145. [Example project](/examples/rpc.guides.websocket-subscriptions.rust-8.tar.gz) ```rust use alloy::primitives::address; use alloy::providers::{Provider, ProviderBuilder, WsConnect}; use alloy::rpc::types::Filter; use std::{env, error::Error}; use tokio::sync::broadcast::error::RecvError; #[tokio::main] async fn main() -> Result<(), Box> { let ws = WsConnect::new(format!( "wss://rpc.graphops.xyz/v1/evm/1/{}", env::var("GRAPHOPS_API_KEY")? )); let provider = ProviderBuilder::new().connect_ws(ws).await?; let filter = Filter::new() .address(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")) .event("Transfer(address,address,uint256)"); // A block's matching logs arrive together. alloy buffers 16 by default, so make room for a // busy block. let mut subscription = provider.subscribe_logs(&filter).channel_size(1024).await?; // This demonstration stops after the first log. let log = loop { match subscription.recv().await { Ok(log) => break log, // A full buffer drops its oldest logs. Backfill them with eth_getLogs. Err(RecvError::Lagged(skipped)) => eprintln!("Skipped {skipped} logs"), Err(error) => return Err(error.into()), } }; println!("{}", serde_json::to_string(&log)?); Ok(()) } ``` Output ```json { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000013247b1da8", "blockHash": "0x09fffa722f278b60998290f6534850d5b2248e083ada05d332aa684f24f8302e", "blockNumber": "0x1911633", "blockTimestamp": "0x6ae0e2bb", "transactionHash": "0xcd465a3b90aa352f8e1d7e989d9cc2171987ed5ee7a9e10b8cf922948ec3d9a4", "transactionIndex": "0x0", "logIndex": "0x1", "removed": false } ``` Store as your last processed block true when a reorg reverses this log Prerequisites 1. Install Python. 2. Install web3. `python -m pip install web3` 3. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with Python 3.12.12, web3 8.0.0. [Example project](/examples/rpc.guides.websocket-subscriptions.python-6.tar.gz) ```python import asyncio import os from web3 import AsyncWeb3, Web3, WebSocketProvider async def main(): async with AsyncWeb3( WebSocketProvider(f"wss://rpc.graphops.xyz/v1/evm/1/{os.environ['GRAPHOPS_API_KEY']}") ) as w3: await w3.eth.subscribe( "logs", { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # keccak256("Transfer(address,address,uint256)"), the Transfer event topic. "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ], }, ) # This demonstration prints the first notification, then disconnects. async for payload in w3.socket.process_subscriptions(): # to_json serializes web3's HexBytes and AttributeDict values. print(Web3.to_json(payload)) break asyncio.run(main()) ``` Output ```json { "subscription": "0xaaa21ba7cf4afa47a687264a16194fa6", "result": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640", "0x00000000000000000000000023617e59a5925b2a4bf75d73ff6711cd0b29de85" ], "data": "0x0000000000000000000000000000000000000000000000000000002ba4a13967", "blockHash": "0xba80b9e7126dc40fe13ab8eaa2d6830a4982c0d514433696c990b86a281b4313", "blockNumber": 26285618, "blockTimestamp": "0x6ae0e2af", "transactionHash": "0xcf0201a45453ea0cbb49b656fcfacc0ca856e3e6f440e24e01642735b56982b2", "transactionIndex": 0, "logIndex": 10, "removed": false } } ``` The id eth\_subscribe returned One log, shaped like an eth\_getLogs entry Store as your last processed block true when a reorg reverses this log All of a block's matching logs arrive together. alloy buffers 16 notifications per subscription by default, so the Rust example sets a larger `channel_size`. When a subscriber falls further behind than its buffer, `recv` returns `Lagged` with the number of skipped logs. Keep receiving, and backfill the skipped logs with `eth_getLogs` as described in [Recover after a disconnect](#recover-after-a-disconnect). ## Supported subscriptions | Subscription | Use it for | | ------------------------ | ---------------------------------------------------------- | | `newHeads` | New block headers. | | `logs` | Logs matching an address and topic filter. | | `newPendingTransactions` | Pending transaction hashes when available for the network. | | `syncing` | Node sync status updates. | ## Recover after a disconnect *Blocks N+2 and N+3 were produced while the socket was down, and a new subscription only delivers events from N+4 onward. One bounded eth_getLogs request fills the gap; deduplicate by log identity where the ranges meet.* - Processed: N, N+1. - Disconnected: N+2, N+3 missed. - Resubscribed: events resume at N+4. - Backfill: `eth_getLogs` fromBlock N+2 · toBlock N+3. 1. Store the last processed block and log identity as you handle each notification. 2. Reconnect with exponential backoff after a disconnect. 3. Resubscribe with the same filter. 4. Backfill with `eth_getLogs` from the block after the last one you processed up to the first block the new subscription delivers. 5. Deduplicate by `(blockHash, transactionHash, logIndex)` where the backfill and the live stream overlap. 6. Treat logs with `removed: true` as reversals. See [Retries and reorgs](./retries-and-reorgs). The backfill is an ordinary HTTPS request. This one reads a single block of USDC `Transfer` logs: Prerequisites 1. Set GRAPHOPS\_API\_KEY to your GraphOps API key. [Example project](/examples/rpc.guides.fetch-logs.sh-1.tar.gz) ```sh # Block numbers are hex quantities: 0x112a882-0x112a887 is blocks 18,000,002-18,000,007. # The topic is keccak256("Transfer(address,address,uint256)"), the Transfer event signature. curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \ -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \ -H "Content-Type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getLogs", "params": [ { "fromBlock": "0x112a882", "toBlock": "0x112a887", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ] } ] }' ``` Output ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "blockHash": "0x5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "blockNumber": "0x112a882", "blockTimestamp": "0x64ea26a7", "transactionHash": "0x29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d", "transactionIndex": "0x2b", "logIndex": "0xac", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "blockHash": "0x5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "blockNumber": "0x112a882", "blockTimestamp": "0x64ea26a7", "transactionHash": "0xa8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db", "transactionIndex": "0x71", "logIndex": "0x13b", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000180cd4303582d348a54fcb0436d5665f082a23d1", "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43" ], "data": "0x00000000000000000000000000000000000000000000000000000000aad0a340", "blockHash": "0x90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4", "blockNumber": "0x112a883", "blockTimestamp": "0x64ea26b3", "transactionHash": "0x05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3", "transactionIndex": "0x66", "logIndex": "0xde", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000001c727a55ea3c11b0ab7d3a361fe0f3c47ce6de5d", "0x000000000000000000000000c3dcc088aef4373e665b00fc8be45a4b6a4b24a6" ], "data": "0x000000000000000000000000000000000000000000000000000000000686bcda", "blockHash": "0x90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4", "blockNumber": "0x112a883", "blockTimestamp": "0x64ea26b3", "transactionHash": "0xeb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42", "transactionIndex": "0x72", "logIndex": "0xe6", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000043954785980c50768373dc1dafd7ba3a2052036f" ], "data": "0x000000000000000000000000000000000000000000000000000000e8d433aa6e", "blockHash": "0x7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2", "blockNumber": "0x112a884", "blockTimestamp": "0x64ea26bf", "transactionHash": "0xb2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690", "transactionIndex": "0x48", "logIndex": "0xbf", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c880d8be4e94f644f1daa7d3558b50ff5a1c2db7", "0x000000000000000000000000ae2d4617c862309a3d75a0ffb358c7a5009c673f" ], "data": "0x0000000000000000000000000000000000000000000000000000002e90edd000", "blockHash": "0x127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7", "blockNumber": "0x112a885", "blockTimestamp": "0x64ea26cb", "transactionHash": "0x1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d", "transactionIndex": "0x1e", "logIndex": "0x40", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000b86fc5e985facdcae6034beefc6cbdcd8bbd8cb4", "0x00000000000000000000000027f713adefc708469d826ab8f5b1e4672f1a070e" ], "data": "0x0000000000000000000000000000000000000000000000000000000059682f00", "blockHash": "0x127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7", "blockNumber": "0x112a885", "blockTimestamp": "0x64ea26cb", "transactionHash": "0x440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c", "transactionIndex": "0x6d", "logIndex": "0xcb", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000b4eae88d1643e172518dde0ef213c20628798c65", "0x00000000000000000000000038c5a4f22e2e290ba9bbb9e3d9aaf8582f79f4d1" ], "data": "0x0000000000000000000000000000000000000000000000000000000006b78840", "blockHash": "0xdb63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1", "blockNumber": "0x112a887", "blockTimestamp": "0x64ea26e3", "transactionHash": "0x21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea", "transactionIndex": "0x1f", "logIndex": "0x5e", "removed": false }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000579752cff8fee7af09446b2133ee2f9ff10c4fbf", "0x0000000000000000000000001715a3e4a142d8b698131108995174f37aeba10d" ], "data": "0x000000000000000000000000000000000000000000000000000000001d0221cd", "blockHash": "0xdb63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1", "blockNumber": "0x112a887", "blockTimestamp": "0x64ea26e3", "transactionHash": "0xd78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c", "transactionIndex": "0x57", "logIndex": "0xb3", "removed": false } ] } ``` Every matching log in the requested block range USDC contract Transfer event signature Transfer amount: 1,075,000,000 base units, 1,075 USDC Block in decimal: 18,000,002 ## Next steps * [Retries and reorgs](./retries-and-reorgs): Reverse logs from reorganized blocks and retry failed requests safely. * [Authentication](../authentication): Protect route-token URLs or send the key in a header instead. * [Supported methods](../methods#websocket-subscriptions): Check `eth_subscribe` types and parameters. --- # Cursors and reorgs Source: https://docs.graphops.xyz/substreams/concepts/cursors-and-reorgs Decide how your consumer keeps stored data complete and correct when streams disconnect and the chain reorganizes. Two rules cover both: save each cursor in the same transaction as the output it follows, and revert writes after the last valid block when an undo signal arrives. ## Cursor model Each `BlockScopedData` message carries one block's output and a cursor. The cursor is the resume token for the stream position after that block. Reconnecting with it continues at the next block. Commit in this order: 1. Apply the block's output to durable state. 2. Save the cursor in the same transaction when possible. 3. After a disconnect, resume from the latest committed cursor. A cursor saved before its output commits can skip that block after a restart. A cursor kept only in memory makes the consumer replay work from an earlier block, so writes must also be idempotent. ## Reorg and undo model Near the chain head, a block can be replaced by a different block at the same height. Substreams then sends a `BlockUndoSignal` that names the last valid block and its cursor. Your consumer must revert everything it wrote after that block, then continue with the replacement blocks. _Blocks N+1 and N+2 were replaced. The undo signal names N as the last valid block, so the consumer reverts what it wrote after N, stores cursor cN, and applies the replacement blocks._ - Before: N, N+1, N+2 - After the reorg: N, N+1′, N+2′ - `BlockUndoSignal`: last valid block N · cursor cN | Message | Meaning | Required action | | ----------------- | ------------------------------------------------------ | --------------------------------------------------------------- | | `BlockScopedData` | Output for one block, with its cursor. | Apply the output and save the cursor after commit. | | `BlockUndoSignal` | Reorg correction with the last valid block and cursor. | Revert local state to the last valid block and save its cursor. | | `ModulesProgress` | Progress for active jobs and stages. | Use for monitoring and backfill visibility. | To revert, keep enough history to undo recent writes, for example by recording the block number on each row. If your workload cannot revert writes, stream final blocks only; output then trails the chain head. ## The full sequence _Commit each block's output together with its cursor. On an undo signal, revert to the last valid block and keep its cursor, then resume from the stored cursor after a reconnect._ 1. BlockScopedData (block N · cursor cN): Commit block N's output and cursor cN in one transaction. 2. BlockScopedData (block N+1 · cursor cN+1): Commit block N+1's output and cursor cN+1 in one transaction. 3. BlockUndoSignal (last valid block N · cursor cN): Revert everything after block N and store cursor cN. 4. Reconnect (cursor cN): Resume from the stored cursor; the stream continues after block N. ## Practical consequences * Store cursors durably, not only in process memory. * Make writes idempotent by block number, block hash, ordinal, or a package-specific key. * Handle undo signals before claiming exactly-once materialization. * Reconnect with backoff, and resume from the latest committed cursor. ## Next steps * [Resume with cursors](/substreams/guides/resume-with-cursors): Apply the commit order in your consumer. * [Run a sink](/substreams/guides/run-a-sink): Use a standard sink that stores cursors and handles undo for you. * [API integration](/substreams/api): Check every message type a stream can send. --- # Packages and modules Source: https://docs.graphops.xyz/substreams/concepts/packages-and-modules Choose the package and output module your stream runs, and keep that choice fixed in production. A Substreams package is the data pipeline: it decides which blocks are read, how they are transformed, and the output type your consumer decodes. ## What a package contains A package is a compiled `.spkg` file. You can reference one from the Substreams registry, by URL, or from a local file. | Term | Meaning | | ------------- | -------------------------------------------------------------------------------------- | | Package | A compiled `.spkg` containing a manifest, modules, Protobuf descriptors, and metadata. | | Manifest | The source configuration that defines modules, inputs, binaries, and outputs. | | Module | A map or store function in the package. | | Map module | Emits transformed data for each block. | | Store module | Maintains keyed state that other modules read. | | Output module | The module a consumer streams from. | _A package's map modules transform each block and its store modules keep keyed state. Your consumer streams from one output module. Module names here are illustrative; pin the package version you run._ - Source: Blocks - Map module `map_events`: Transforms each block - Store module `store_totals`: Keeps keyed state - Output module `db_out`: Your consumer streams this ## Find a package [substreams.dev](https://substreams.dev/packages) is the public package registry. Use it to find package names, versions, modules, output types, and package documentation. In the GraphOps platform, [Discover Substreams](/substreams/guides/discover-packages) lists popular packages for supported networks and shows a pinned release's modules and output types. You do not upload packages to GraphOps. When you run a package against a GraphOps endpoint, standard Substreams tooling resolves the package reference, whether it is a registry package or an unpublished `.spkg`. Publish packages through the standard Substreams workflow and the [substreams.dev registry](https://substreams.dev/packages). ## Keep your pipeline on a known version Pin an explicit package version or `.spkg` reference in your consumer configuration so a package update does not change your pipeline unexpectedly. GraphOps does not pin the package for you. * Record the package reference, module name, start block, and endpoint in deployment config. * Confirm the package supports a network before you run it there. * Regenerate Protobuf types when you move to a release with a different output type. * Keep the manifest and output type available to the team maintaining your consumer. ## Next steps * [Discover Substreams packages](/substreams/guides/discover-packages): Inspect a release's modules and copy its pinned URL. * [Stream USDC transfers](/substreams/guides/stream-usdc-transfers): Stream an output module with a module parameter. * [Sinks and materialization](/substreams/concepts/sinks-and-materialization): Choose where the output module's data is stored. --- # Sinks and materialization Source: https://docs.graphops.xyz/substreams/concepts/sinks-and-materialization Decide how a block stream becomes data your application can query. A sink writes Substreams output to your database, files, or queue and tracks its cursor so it can recover after a disconnect. You choose between a standard sink and your own consumer. ## Consumption modes Run a consumer or sink in your own infrastructure, or contact GraphOps about enterprise sink hosting. | Mode | GraphOps support | Use it for | | ------------------ | ---------------- | ------------------------------------------------------------------ | | Direct gRPC stream | Self-service | Application consumers and custom materializers. | | Customer-run sink | Self-service | Writing to SQL, files, queues, or custom stores. | | Hosted sink | Enterprise | GraphOps-managed sink hosting. Contact support in the platform UI. | ## Choose a sink Each destination has a standard Substreams tool. The CLI sinks take your GraphOps endpoint with `-e` and read your API key from `SUBSTREAMS_API_KEY`. | Destination | Tool | Guide | | ---------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Your application | Substreams libraries for JavaScript, Go, Rust, or Python | [Substreams:Stream](https://docs.substreams.dev/how-to-guides/sinks/stream) | | Postgres or ClickHouse | `substreams sink postgres` or `substreams sink clickhouse`, built into the CLI | [Run a sink](/substreams/guides/run-a-sink), [Substreams:SQL](https://docs.substreams.dev/how-to-guides/sinks/sql) | | Files | `substreams sink protojson` for JSONL; `substreams-sink-files` for CSV or Parquet | [ProtoJSON](https://docs.substreams.dev/how-to-guides/sinks/protojson), [Files](https://docs.substreams.dev/how-to-guides/sinks/files) | | Google Cloud Pub/Sub | `substreams sink pubsub` | [Substreams:PubSub](https://docs.substreams.dev/how-to-guides/sinks/pubsub) | The SQL sink needs an output module that emits `DatabaseChanges`, usually `db_out`, or Protobuf output annotated for relational mappings. ## Sink responsibilities A standard sink takes care of most of these. If you write your own consumer, it must do all of them. _A sink decodes the output module, applies each block's data to durable state, and persists the cursor only after that commit. On an undo signal it rolls local state back to the last valid block._ A production sink should: * Decode the output module type. * Apply block-scoped data to durable state. * Persist cursors after durable commits. * Handle undo signals and rollback local state. * Expose metrics for processed blocks, bytes out, bytes read, bytes written, lag, retries, and failures. * Bound worker concurrency according to the organization's limits. ## Next steps * [Run a sink](/substreams/guides/run-a-sink): Write a package's output to Postgres and verify the rows. * [Resume with cursors](/substreams/guides/resume-with-cursors): Apply the commit order in a consumer you write yourself. * [Usage and limits](/substreams/usage-and-limits): Size sink concurrency to your organization's limits. --- # Backfill a range Source: https://docs.graphops.xyz/substreams/guides/backfill-a-range Load a protocol's history into your database before you follow the live chain. This guide runs a bounded block range, checks the output, and then shows how to split a long history into ranges you can retry independently and continue live from the same consumer. _GraphOps can execute a historical range in parallel segments, but your consumer still receives output in block order and saves a cursor as it commits. Leave the stop block open and the same stream continues with live blocks._ 1. Start block: GraphOps can run the historical range in parallel segments. 2. Output in block order: the consumer commits each block and saves its cursor. 3. Chain head: with no stop block, live blocks follow on the same stream. ## Prerequisites * A Substreams API key. * A package and output module that support the target network. * A destination that can commit output idempotently, so a retried range does not duplicate rows. ## Run a bounded range Set a start block and a stop block. This example streams `filtered_events` for blocks `18000002` up to the stop block `18000008` on Ethereum mainnet. The CLI tab is a complete command. The TypeScript, Go, Rust, and Python tabs are complete consumers. Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.guides.backfill-a-range.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}" # Backfill USDC Transfer events (--params) from 18000002 up to, not including, 18000008. 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 \ -o jsonl ``` Output ```json { "@module": "filtered_events", "@block": 18000002, "@type": "sf.substreams.ethereum.v1.Events", "@data": { "clock": { "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "number": "18000002", "timestamp": "2023-08-26T16:21:59Z" }, "events": [ { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "index": 1, "blockIndex": 172, "ordinal": "2582" }, "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d" }, { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "index": 2, "blockIndex": 315, "ordinal": "5139" }, "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db" } ] } } { "@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,002 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 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 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 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> { 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 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 ## Check the output The CLI prints a `filtered_events` message for every block in the range, in block order, with no `events` field for blocks without matching USDC Transfers. It exits when it reaches the stop block. Each message has the same envelope as the [quickstart output](/substreams/getting-started#inspect-the-output): the block number in `@block` and the module output in `@data`. Before you scale up, compare the matching events in the requested range with your destination and confirm that rerunning the same range leaves the row count unchanged. ## Split a long history A single stream can cover a long range, but one failure late in the range means repeating it. For large histories, plan the work as bounded ranges: ### Choose range boundaries \[#choose-range-boundaries] Pick a range size you can rerun in an acceptable time. Adjacent ranges share a boundary block number; the stop block of one range is the start block of the next. ### Run and commit each range \[#run-and-commit-each-range] Run each range with its own start and stop block. Write output idempotently, keyed by block number, block hash, ordinal, or a package-specific ID. ### Record progress \[#record-progress] After a range commits, store the range, package reference, module name, endpoint, and final cursor in your backfill state. A failed range can then be retried alone. ### Continue live \[#continue-live] When the history is loaded, start a stream at the last stop block and omit `--stop-block`. The stream continues with new blocks as they arrive. Run ranges within your organization's concurrency limit. If you see rate-limit or resource errors, run fewer ranges at once. Compare output volume with the current [credit rates](/substreams/usage-and-limits). ## Next steps * [Resume with cursors](/substreams/guides/resume-with-cursors): Restart the live stream without skipping blocks. * [Run a sink](/substreams/guides/run-a-sink): Backfill straight into Postgres with a standard sink. * [Usage and limits](/substreams/usage-and-limits): Check the rates and limits that apply to large backfills. --- # Discover Substreams Source: https://docs.graphops.xyz/substreams/guides/discover-packages Choose a package to stream before you write a consumer, and leave with a pinned release you can run. Search by package or network name, and filter by network. You need membership in a GraphOps organization. [Discover Substreams](https://platform.graphops.xyz/organizations/current/services/substreams/discover) _Find a package, open an exact release, check its modules and output types, then copy the pinned package URL into your consumer or the endpoint wizard._ 1. Find: Search or filter by network 2. Open a release: `@` 3. Inspect: Modules and output types 4. Run: Pinned URL and endpoint ## Choose a package The catalog lists the most-downloaded packages on each supported Substreams network, ranked by **Registry downloads** published by [substreams.dev](https://substreams.dev/packages). Each bar compares a package with the most-downloaded package in the catalog. Downloads measure registry popularity, not query volume or whether a package fits your application. To explore a release that is not listed, look it up. To look up a release you already know, paste it into the same search box and apply it: * its name and version, `@`; * its substreams.dev release page, `https://substreams.dev/packages//`; * its registry download URL, `https://spkg.io/v1/packages//`. The page offers **Explore package** for that release, even when it is not among the popular packages. Only releases published in the substreams.dev registry with an exact version can be explored. Other `.spkg` URLs, such as GitHub release downloads, and `latest` references cannot be explored; you can still run them with your Substreams client. ## Inspect a package Choose a package name, or **View Substreams package** in its row's actions menu, to open it. **Package metadata** shows the pinned package URL to copy into your consumer, the network the package declares and whether GraphOps Substreams serves it, its modules, earliest start block, block source, sink module and source repository. The package's modules and output types have their own sections. **README** shows the documentation the package author published with the release, such as what it emits, the params it accepts and how to run it. A long README opens as a preview; choose **Show full README** to read the rest. Links open in a new tab. Images and links relative to the package's repository appear as text, so open the repository to see them. **Modules** starts with a graph of how data flows through the package, so you can see what the module you plan to stream depends on. Data flows from left to right: block sources such as `sf.ethereum.type.v2.Block` and params feed the modules that read them, through to the sink module. Each module shows its kind, the update policy of a store, its output type and its start block when that differs from the package's earliest. A `get` connection reads a store's current values, and `deltas` reads each block's changes. A dashed **filter** connection comes from a block index that limits which blocks a module processes. Hover over or focus a module to highlight its inputs and the modules that consume it. Drag to pan, pinch or use **Ctrl + scroll** to zoom, or choose **Fit** to see the whole graph. Choose **Full screen** beside the zoom buttons to expand the graph to the whole window; choose **Exit full screen** or press **Esc** to return. Choose the eye icon beside a module's name to jump to its details below. Below the graph, each module shows its kind, start block, output type, the update policy of a store, and its inputs and block filter. Choose an input module to jump to its details. Modules that come from a package this package imports are listed under that package's name and version. Stream a map module from your client; store modules hold intermediate state for other modules. **Output types** shows the fields of each map module's output message and the messages it contains, grouped by protobuf package, with each field's number, so you can generate or check your consumer's types before running the package. The sink module's output is marked **Sink output**. Select an output type in the graph or module details to open its fields in **Output types** when the package publishes its definition. The same links connect message fields to their types, including messages contained in lists or maps. Types without a displayed definition appear as plain text. ## Get an endpoint Choose **Get query endpoint** in a package row's actions menu or the package page's **Actions** menu. The wizard selects the package's network and package, and opens on choosing an API key; the summary at the top shows them, and **Edit** changes them. The example command uses the package. Replace the ``, `` and `` placeholders with values from the package page, then run the example. When you start from **Networks** instead, the example and the wizard preselect the network's most downloaded package. Choose another package under **Example package**. In the wizard, paste another package URL, or clear the field to keep the package placeholder. A network without registry packages keeps the placeholders. The wizard's address keeps your choices, except the API key token, so you can reload it or share it with a teammate. Supply a saved API key, or create one if you are an Owner or Developer. Closing the wizard returns you to the page you started from, with its search and filters preserved. ## Next steps * [Packages and modules](/substreams/concepts/packages-and-modules): Choose the output module and keep the version pinned. * [Backfill a range](/substreams/guides/backfill-a-range): Run the package over historical blocks. * [Run a sink](/substreams/guides/run-a-sink): Write the package's output to Postgres. --- # 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. Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. 3. Replace `` 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 "" \ -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 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 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 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> { 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 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. --- # Run a sink Source: https://docs.graphops.xyz/substreams/guides/run-a-sink Build a queryable transfer history in Postgres from a Substreams package. You will run the SQL sink locally, stream a bounded Ethereum range through GraphOps, and verify the stored rows. The SQL sink is part of the Substreams CLI as `substreams sink postgres` and `substreams sink clickhouse`; there is no separate binary to install. The example uses `substreams-template@v0.3.1`, a public Ethereum package with a `db_out` module that emits `sf.substreams.sink.database.v1.DatabaseChanges` and a SQL `sink` configuration. You run the sink in this guide. For GraphOps-hosted sinks, contact support about enterprise options. _The package runs on GraphOps. You run setup once, run the sink over a bounded range, then query the rows it wrote. The sink stores its cursor in the same database._ - Package: `substreams-template@v0.3.1` (Output module db_out) - GraphOps endpoint: `1.evm.substreams.graphops.xyz` - Postgres: your database, tables `transfer` and `cursors` 1. setup: `sink postgres setup`, tables and cursor storage 2. run: `-s 12292922 -t 12293022`, rows and cursor per block 3. verify: `select count(*)`, rows in transfer ## Prerequisites * A Substreams API key. * The Substreams CLI. Install it with `Download substreams 1.25.0 from https://github.com/streamingfast/substreams/releases/tag/v1.25.0 and verify the release checksum.`, or follow [Install Substreams CLI](https://docs.substreams.dev/how-to-guides/installing-the-cli). * Docker, or an existing Postgres database. * A package with SQL sink output. This guide uses `substreams-template@v0.3.1`. ## Start Postgres Start a local Postgres instance for the sink. The sample credentials are for local development only; use managed secrets for production deployments. Tested with postgres 16-alpine\@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea. ```sh docker run -d --name "${POSTGRES_CONTAINER:-graphops-substreams-postgres}" \ -e POSTGRES_USER=dev-node \ -e POSTGRES_PASSWORD=local-dev-only-password \ -e POSTGRES_DB=dev-node \ -p "${POSTGRES_BINDING:-5432:5432}" \ --health-cmd "pg_isready -h 127.0.0.1 -U dev-node -d dev-node" \ public.ecr.aws/docker/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea ``` Set the connection string (DSN) for the sink and for `psql`: ```sh export DSN="postgres://dev-node:local-dev-only-password@localhost:${POSTGRES_PORT:-5432}/dev-node?sslmode=disable" ``` The sink takes the DSN from `--dsn`, or from the `SUBSTREAMS_SINK_DSN` environment variable when you omit the flag. See the [DSN reference](https://docs.substreams.dev/reference-material/sql/sql/dsn-reference) for Postgres and ClickHouse options. ## Configure GraphOps access Set your API key. The sink reads it from `SUBSTREAMS_API_KEY`, like every other `substreams` command: ```sh export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}" ``` You pass the GraphOps Ethereum mainnet endpoint with `-e` when you run the sink. ## Initialize tables Run `setup` once per database. It creates the sink's system tables, `cursors` and `substreams_history`, and runs the package's `schema.sql`. Prerequisites 1. Install the Substreams CLI. Tested with substreams-template v0.3.1, substreams 1.25.0. ```sh substreams sink postgres setup "substreams-template@v0.3.1" --dsn "$DSN" ``` The package schema creates application tables such as: ```sql CREATE TABLE transfer ( id TEXT NOT NULL, trx_hash TEXT NOT NULL, "from" TEXT NOT NULL, "to" TEXT NOT NULL, token_id BIGINT NOT NULL, ordinal BIGINT NOT NULL, PRIMARY KEY (id) ); ``` ## Run the sink Run a bounded range first: Prerequisites 1. Install the Substreams CLI. Tested with substreams-template v0.3.1, substreams 1.25.0. ```sh substreams sink postgres "substreams-template@v0.3.1" \ -e "https://1.evm.substreams.graphops.xyz" \ --start-block 12292922 \ --stop-block 12293022 \ --dsn "$DSN" ``` This streams the package's configured SQL output module, writes rows to Postgres, and stores the sink cursor in the database. The package's `db_out` module is selected from the manifest's `sink` configuration: ```yaml sink: module: db_out type: sf.substreams.sink.sql.v1.Service config: schema: "./schema.sql" ``` To follow the chain after the initial range, omit the stop block: Prerequisites 1. Install the Substreams CLI. Tested with substreams-template v0.3.1, substreams 1.25.0. ```sh substreams sink postgres "substreams-template@v0.3.1" \ -e "https://1.evm.substreams.graphops.xyz" \ --start-block 12292922 \ ${STOP_BLOCK:+--stop-block "$STOP_BLOCK"} \ --dsn "$DSN" ``` When you restart the sink against the same database, it resumes from the stored cursor. ## Verify rows Inspect the tables: ```sh psql "$DSN" -c '\dt' ``` Count materialized transfer rows: ```sh psql "$DSN" -c 'select count(*) as transfer_count from transfer;' ``` Output ```text transfer_count ---------------- 1 (1 row) ``` Column alias from the registered query Rows committed by the local sink for the range Read a few rows: ```sh psql "$DSN" -c 'select trx_hash, "from", "to", token_id, ordinal from transfer order by ordinal limit 5;' ``` ## Use your own package The sink chooses its mode from the output module's type. Your package must use one of them: | Mode | Requirement | Notes | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Database Changes | The output module emits `sf.substreams.sink.database.v1.DatabaseChanges`, commonly from a `db_out` module, and the package provides `schema.sql`. | Supports inserts, updates and upserts. | | Relational Mappings | The output Protobuf messages carry table and key annotations. | Insert-only; the schema is derived from the Protobuf. In beta: Postgres is stable, ClickHouse is in development. | For Database Changes, the package manifest names the module, engine and schema in its sink configuration: ```yaml sink: module: db_out type: sf.substreams.sink.sql.v1.Service config: engine: postgres schema: schema.sql ``` Then run: Prerequisites 1. Replace `` with your Substreams package URL, `` with the first block to process, and `` with the block to stop before. ```sh substreams sink postgres setup "" --dsn "$DSN" substreams sink postgres "" \ -e "$GRAPHOPS_ENDPOINT" \ --start-block \ --stop-block \ --dsn "$DSN" ``` To sink a module other than the configured one, name it after the package reference. For ClickHouse, use `substreams sink clickhouse` with a `clickhouse://` DSN; ClickHouse applies chain reorgs with a delay. Follow [Substreams:SQL](https://docs.substreams.dev/how-to-guides/sinks/sql) for both modes, Protobuf annotations and engine options. If you ran the standalone `substreams-sink-sql` binary, the CLI keeps its cursor tables and schemas, so it resumes where the binary stopped. See [Migrating from substreams-sink-sql](https://docs.substreams.dev/how-to-guides/sinks/sql/migration) for the command and flag changes. ## Before production * Start with bounded ranges before running an open-ended live sink. * Use a dedicated database per sink. * Store the package reference, endpoint, range, and Substreams CLI version in release records. * Let the sink manage its cursor and history tables; back up those tables with the materialized data. * Keep package versions pinned in production. * Monitor blocks processed, bytes out, row flush failures, retries, and sink lag. Set `STOP_BLOCK` to an exclusive block height when you want the continuous command to finish after a bounded range. Leave it unset to follow the chain. For a different package, set `GRAPHOPS_ENDPOINT` to its chain endpoint and use a package that emits `DatabaseChanges`. ## Next steps * [Sinks and materialization](/substreams/concepts/sinks-and-materialization): Choose a sink for files, Pub/Sub, or ClickHouse. * [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs): Understand how the sink's cursor and history tables keep rows correct. * [Substreams:SQL](https://docs.substreams.dev/how-to-guides/sinks/sql): Look up SQL sink flags, Protobuf annotations, and engine options. --- # Stream events from Ethereum blocks Source: https://docs.graphops.xyz/substreams/guides/stream-ethereum-blocks Stream USDC Transfer events from Ethereum blocks. This guide runs the pinned `ethereum-common@v0.4.0` package and decodes its `filtered_events` output. Blocks without matching events can be omitted; an emission is not promised for every block. ## Prerequisites * A Substreams API key. ## Stream a block range Run `filtered_events` from the pinned `ethereum-common` package against the Ethereum mainnet endpoint. The CLI tab is a complete command. The TypeScript, Go, Rust, and Python tabs are complete consumers. Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.api.sh-2.tar.gz) ```sh # The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header. export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}" # Stream USDC Transfer events (--params) for six blocks from 18000002 as JSON lines. 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 +6 \ -o jsonl ``` Output ```json { "@module": "filtered_events", "@block": 18000002, "@type": "sf.substreams.ethereum.v1.Events", "@data": { "clock": { "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "number": "18000002", "timestamp": "2023-08-26T16:21:59Z" }, "events": [ { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "index": 1, "blockIndex": 172, "ordinal": "2582" }, "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d" }, { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "index": 2, "blockIndex": 315, "ordinal": "5139" }, "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db" } ] } } { "@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,002 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 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 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 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> { 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 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 ## Read the output The CLI places the module's fields inside `@data`; the language consumers use `data` and include a checkpoint cursor for complete blocks. * `@block` is the block the message covers. Messages arrive in block order. * `@type` names the Protobuf message to decode in a client library. * `@data` holds the module output; this module returns Ethereum logs and their transaction hashes. ## Follow the chain For a continuous consumer, remove the bounded stop block. Start from the block your application needs, and save each block's cursor after you commit its output so you can resume after a disconnect. Before you write to durable state, pin the package version and read [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs): blocks near the chain head can be replaced. ## Next steps * [Stream USDC transfers](/substreams/guides/stream-usdc-transfers): Stream one contract's events with a module parameter. * [Resume with cursors](/substreams/guides/resume-with-cursors): Commit output with its cursor and restart safely. * [API integration](/substreams/api): Check the stream inputs and messages your client handles. --- # Stream USDC transfers Source: https://docs.graphops.xyz/substreams/guides/stream-usdc-transfers Stream the activity of one Ethereum contract, USDC, as the input to a transfer index or activity feed. This guide passes the contract address to a package module as a parameter, then shows how to pick `Transfer` events out of the output. The module returns raw Ethereum logs with transaction hashes. Decode the Transfer topics and data to obtain sender, recipient, and amount. Your consumer filters and stores the transfer records, then commits them with the block's cursor. ## Prerequisites * A Substreams API key. * A consumer that can decode the module's `sf.substreams.ethereum.v1.Events` output type. * A durable store for transfer rows and cursors. ## Stream the contract's events USDC on Ethereum mainnet: ```text 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 ``` `--params` passes an address and signature query to `filtered_events`, which returns matching USDC Transfer logs. The CLI tab is a complete command over six blocks. The other tabs are complete consumers. Each requests the same package, module, filter, and finite range. Prerequisites 1. Install the Substreams CLI. 2. Set GRAPHOPS\_API\_KEY to your GraphOps API key. Tested with ethereum-common v0.4.0, substreams 1.25.0. [Example project](/examples/substreams.api.sh-2.tar.gz) ```sh # The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header. export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}" # Stream USDC Transfer events (--params) for six blocks from 18000002 as JSON lines. 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 +6 \ -o jsonl ``` Output ```json { "@module": "filtered_events", "@block": 18000002, "@type": "sf.substreams.ethereum.v1.Events", "@data": { "clock": { "id": "5b94c39ad7eecbf043f62a4a76c5abee87195207df98ae16eb3f289b2267f141", "number": "18000002", "timestamp": "2023-08-26T16:21:59Z" }, "events": [ { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000e673bda19a529d9ff13aeddae0e08c0fda8b5488", "0x00000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640" ], "data": "0x00000000000000000000000000000000000000000000000000000000401332c0", "index": 1, "blockIndex": 172, "ordinal": "2582" }, "txHash": "29ec08dc5eed0e1cd411b29c52607661155087ffa107bae7f5e4a79f5306be1d" }, { "log": { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000a40bb1c47f6dd27142a2bd7c93bfa98db9d1f5c5", "0x000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc" ], "data": "0x0000000000000000000000000000000000000000000000000000000011348e5d", "index": 2, "blockIndex": 315, "ordinal": "5139" }, "txHash": "a8b07b56f7bb4ea52eecf5a125c75f143955a8e57e1ea17d2d8a48830e7d67db" } ] } } { "@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,002 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 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 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 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> { 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 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 ## Read the output CLI JSON places events inside `@data` and identifies the output type with `@type`. The CLI encodes log bytes as hexadecimal; the language consumers use standard Protobuf JSON with base64 bytes. Transaction hashes are strings. * `topics[0]` identifies the event. `ddf252ad…` is the ERC-20 `Transfer` signature; keep events that match it. * `topics[1]` and `topics[2]` are the sender and recipient, left-padded to 32 bytes. The last 40 hex characters are the address. * `txHash` links the event to its transaction. The `data` field contains the transfer amount as a 32-byte unsigned integer. USDC has six decimals, so divide its decoded integer by 1,000,000 to obtain the token amount. ## Before production * Commit a block's transfer rows and its cursor together. * Handle undo signals if you follow blocks near the chain head; see [Cursors and reorgs](/substreams/concepts/cursors-and-reorgs). * Backfill history with finite ranges, then continue live; see [Backfill a range](/substreams/guides/backfill-a-range). * Watch bytes out. Event-heavy modules can produce large responses. ## Next steps * [Backfill a range](/substreams/guides/backfill-a-range): Load the contract's event history before you follow the chain. * [Packages and modules](/substreams/concepts/packages-and-modules): Choose a package and output module that fit the data you store. * [Usage and limits](/substreams/usage-and-limits): Estimate the cost of an event-heavy stream.