> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bloklake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Client libraries

> Official clients for TypeScript/Node.js, Python and Rust: typed queries in, JSON rows out, Arrow on the wire.

The official clients are the easiest way to use Bloklake. You write queries with typed filters
and field names; the response travels as Apache Arrow (about 2–2.5× smaller than JSON, so
[cheaper](https://docs.bloklake.com/billing/how-billing-works.md)) and the client decodes it into plain JSON rows with
exactly the values [`/query/json`](https://docs.bloklake.com/responses/json) would return.

They also do the things every production client needs:

* **Pagination**: follow `next_block` over any range, reusing the
  [prepared query](https://docs.bloklake.com/querying/prepared-queries.md) and re-sending it if it expired.
* **Retries**: rate limits (`429`), busy servers (`503`), server errors and dropped connections,
  honoring `Retry-After`.
* **Integrity**: a response that ends early or carries fewer rows than announced is an error and
  is retried, never returned as a short result.
* **Types**: field names are checked. In TypeScript, each row is typed with exactly the fields
  you selected.

## Install

```bash TypeScript / Node.js theme={null}
npm install @bloklake/client
```

```bash Python theme={null}
pip install bloklake
```

```bash Rust theme={null}
cargo add bloklake
cargo add tokio --features full
```

Node.js 18+ (or Bun, Deno), Python 3.9+, Rust 2024 edition. Set your [API key](https://docs.bloklake.com/account/api-keys.md)
in the `BLOKLAKE_API_KEY` environment variable, or pass it to the constructor.

## Your first query

```typescript TypeScript theme={null}
import { Bloklake } from "@bloklake/client";

const bl = new Bloklake({ chain: "eth" }); // API key from BLOKLAKE_API_KEY

const page = await bl.query({
  fromBlock: 19426000,
  toBlock: 19426009,
  logs: [{
    address: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
    topic0: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
  }],
  fields: { log: ["blockNumber", "transactionHash", "topic1", "topic2", "data"] },
});

console.log(page.logs.length, page.logs[0].blockNumber, page.logs[0].data);
console.log("next page starts at", page.meta.nextBlock);
```

```python Python theme={null}
from bloklake import Bloklake

bl = Bloklake(chain="eth")  # API key from BLOKLAKE_API_KEY

page = bl.query({
    "from_block": 19426000,
    "to_block": 19426009,
    "logs": [{
        "address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
        "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
    }],
    "fields": {"log": ["block_number", "transaction_hash", "topic1", "topic2", "data"]},
})

print(len(page.logs), page.logs[0]["block_number"], page.logs[0]["data"])
print("next page starts at", page.meta.next_block)
```

```rust Rust theme={null}
use bloklake::{Client, LogField, LogFilter, Query};

#[tokio::main]
async fn main() -> bloklake::Result<()> {
    let client = Client::builder().chain("eth").build()?; // API key from BLOKLAKE_API_KEY

    let query = Query::new()
        .from_block(19426000)
        .to_block(19426009)
        .log(LogFilter::new()
            .address(["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"])
            .topic0(["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]))
        .log_fields([LogField::BlockNumber, LogField::TransactionHash, LogField::Topic1, LogField::Topic2, LogField::Data]);

    let page = client.query(&query).await?;
    let first = &page.logs[0];
    println!("{} {} {}", page.logs.len(), first.block_number.unwrap(), first.data.as_deref().unwrap());
    println!("next page starts at {}", page.meta.unwrap().next_block);
    Ok(())
}
```

```text theme={null}
72 19426000 0x000000000000000000000000000000000000000000000000000000016a6cff5d
next page starts at 19426010
```

Pick the chain with `chain` (`"eth"`, `"rh"`), or pass a full `baseUrl` / `base_url`. One client
talks to one chain; create one per chain.

## Reading a whole range

`paginate` follows `next_block` until `toBlock` (or the chain head if you leave `toBlock` out),
one page at a time, so memory stays flat however long the range is. `collect` returns all pages
merged into one, for ranges that fit in memory.

```typescript TypeScript theme={null}
import { Bloklake } from "@bloklake/client";

const bl = new Bloklake({ chain: "eth" });
const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

let total = 0;
for await (const page of bl.paginate({
  fromBlock: 19426000,
  toBlock: 19427499,
  logs: [{ address: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"], topic0: [TRANSFER] }],
  fields: { log: ["blockNumber", "logIndex", "topic1", "topic2", "data"] },
})) {
  total += page.logs.length; // store(page.logs)
  console.log(`blocks ${page.meta.fromBlock}..${page.meta.toBlock}: ${page.logs.length} logs`);
}
console.log(total, "transfers");
```

```python Python theme={null}
from bloklake import Bloklake

bl = Bloklake(chain="eth")
TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

total = 0
for page in bl.paginate({
    "from_block": 19426000,
    "to_block": 19427499,
    "logs": [{"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"], "topic0": [TRANSFER]}],
    "fields": {"log": ["block_number", "log_index", "topic1", "topic2", "data"]},
}):
    total += len(page.logs)  # store(page.logs)
    print(f"blocks {page.meta.from_block}..{page.meta.to_block}: {len(page.logs)} logs")
print(total, "transfers")
```

```rust Rust theme={null}
use bloklake::{Client, LogField, LogFilter, Query};

const TRANSFER: &str = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

#[tokio::main]
async fn main() -> bloklake::Result<()> {
    let client = Client::builder().chain("eth").build()?;
    let query = Query::new()
        .from_block(19426000)
        .to_block(19427499)
        .log(LogFilter::new().address(["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]).topic0([TRANSFER]))
        .log_fields([LogField::BlockNumber, LogField::LogIndex, LogField::Topic1, LogField::Topic2, LogField::Data]);

    let mut total = 0;
    let mut pages = client.paginate(&query);
    while let Some(page) = pages.next().await? {
        total += page.logs.len(); // store(&page.logs)
        let m = page.meta.unwrap();
        println!("blocks {}..{}: {} logs", m.from_block, m.to_block, page.logs.len());
    }
    println!("{total} transfers");
    Ok(())
}
```

After the first page, only the prepared-query id and the range are sent.

## What you get back

A page has `logs`, `transactions` and `blocks` (empty when not requested) and `meta`:
`fromBlock`, `toBlock`, `nextBlock`, `objects`, `archiveHeight`, `finalizedBlock`,
`toBlockHash`, `queryId` and `unavailableFields` (snake\_case in Python and Rust). See
[Response metadata](https://docs.bloklake.com/responses/metadata.md).

Each client names fields in its own language's style:

| | Query | Rows |
| - | - | - |
| TypeScript | `blockNumber` | `log.blockNumber`: the names you selected, nested objects too (`accessList[0].storageKeys`) |
| Python | `"blockNumber"` or `"block_number"` | `log["block_number"]`: the response's column names |
| Rust | `LogField::BlockNumber` | `log.block_number` |

In Python, every query key may be written in snake\_case (`"from_block"`, `"address_set"`,
`"block_number"`) or in the API's camelCase; the client sends camelCase either way. Give each key
one way only.

Values follow the [JSON encoding](https://docs.bloklake.com/responses/json.md):

| Field kind | TypeScript | Python | Rust |
| - | - | - | - |
| block numbers, indexes, gas, timestamps | `number` | `int` | `u64` / `u32` / `u8` |
| amounts (`value`, gas prices, fees, uint256) | decimal `string` | decimal `str` | decimal `String` |
| hashes, addresses, topics, `input`, `data` | `0x` hex `string` | `0x` hex `str` | `0x` hex `String` |
| `withdrawals`, `access_list`, `authorization_list` | arrays of objects | lists of dicts | `Vec` of structs |
| not selected | absent (a type error) | absent | `None` |
| not available or not applicable | `null` | `None` | `None` |

In Rust, `query` returns typed structs (`Log`, `Transaction`, `Block`, every field an
`Option`); `query_raw` returns the rows as `serde_json` objects exactly as `/query/json` would.

> [!TIP]
>
> Amounts are decimal strings because they exceed 64 bits. Use `BigInt(x)` in JavaScript,
> `int(x)` in Python, or parse to `u128` in Rust.

## Sets

```typescript TypeScript theme={null}
import { Bloklake } from "@bloklake/client";

const bl = new Bloklake({ chain: "eth" });
const tokens = ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7"];

const { setId } = await bl.createSet({ kind: "address", values: tokens }); // same values, same id
const page = await bl.query({
  fromBlock: 19426000,
  toBlock: 19426099,
  logs: [{ addressSet: setId, topic0: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] }],
  fields: { log: ["address", "data"] },
});
console.log(setId, page.logs.length);
await bl.deleteSet(setId); // when you no longer need it
```

```python Python theme={null}
from bloklake import Bloklake

bl = Bloklake(chain="eth")
tokens = ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7"]

s = bl.create_set("address", tokens)  # same values, same id
page = bl.query({
    "from_block": 19426000,
    "to_block": 19426099,
    "logs": [{"address_set": s.set_id, "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}],
    "fields": {"log": ["address", "data"]},
})
print(s.set_id, len(page.logs))
bl.delete_set(s.set_id)  # when you no longer need it
```

In Rust: `client.create_set("address", tokens, None)`, then `LogFilter::new().address_set(&set.set_id)`.
See [Sets](https://docs.bloklake.com/querying/sets.md).

## Errors

Errors that retrying can't fix are raised at once, with the API's `status` and `code` (see
[Errors](https://docs.bloklake.com/api-reference/errors.md)) and the response's request id:

```typescript TypeScript theme={null}
import { Bloklake, BloklakeError } from "@bloklake/client";

const bl = new Bloklake({ chain: "eth", maxRetries: 4 }); // 429, 503, 5xx and broken connections are retried

try {
  await bl.query({ fromBlock: 10, toBlock: 9, fields: { block: ["number"] } });
} catch (e) {
  if (!(e instanceof BloklakeError)) throw e;
  console.log(e.status, e.code); // 400 "invalid_query"
  console.log(e.message);        // "400 invalid_query: fromBlock (10) is greater than toBlock (9)"
  if (e.code === "balance_exhausted") { /* alert a person: retrying won't help */ }
}
```

```python Python theme={null}
from bloklake import Bloklake, BloklakeError

bl = Bloklake(chain="eth", max_retries=4)  # 429, 503, 5xx and broken connections are retried

try:
    bl.query({"from_block": 10, "to_block": 9, "fields": {"block": ["number"]}})
except BloklakeError as e:
    print(e.status, e.code, e.message)  # 400 invalid_query fromBlock (10) is greater than toBlock (9)
    if e.code == "balance_exhausted":
        pass  # alert a person: retrying won't help
```

```rust Rust theme={null}
use bloklake::{BlockField, Client, Query};

#[tokio::main]
async fn main() -> bloklake::Result<()> {
    let client = Client::builder().chain("eth").max_retries(4).build()?; // 429, 503, 5xx and broken connections are retried

    let query = Query::new().from_block(10).to_block(9).block_fields([BlockField::Number]);
    match client.query(&query).await {
        Ok(_) => {}
        Err(e) if e.code == "balance_exhausted" => { /* alert a person: retrying won't help */ }
        Err(e) => println!("{} {} {}", e.status, e.code, e.message), // 400 invalid_query fromBlock (10) is greater than toBlock (9)
    }
    Ok(())
}
```

Client-side failures have status `0` and code `network`, `timeout` or `truncated` (after the
retries ran out).

## More calls

| | TypeScript | Python | Rust |
| - | - | - | - |
| One page | `query(q)` | `query(q)` | `query(&q)`, `query_raw(&q)` |
| Every page | `paginate(q)` | `paginate(q)` | `paginate(&q)` → `.next()`, `.next_raw()` |
| All pages merged | `collect(q)` | `collect(q)` | `collect(&q)` |
| Sets | `createSet`, `getSet`, `deleteSet` | `create_set`, `get_set`, `delete_set` | `create_set`, `get_set`, `delete_set` |
| Register a query | `prepare(q)` | `prepare(q)` | `prepare(&q)` |
| Chain range and finality (free) | `status()` | `status()` | `status()` |
| Fields of this chain (free) | `schema()` | `schema()` | `schema()` |
| Key plan, limits, balance (free) | `account()` | `account()` | `account()` |

## Options

| | TypeScript | Python | Rust (builder) |
| - | - | - | - |
| API key | `apiKey` | `api_key` | `.api_key()` |
| Chain or URL | `chain`, `baseUrl` | `chain`, `base_url` | `.chain()`, `.base_url()` |
| Retries (default 4) | `maxRetries` | `max_retries` | `.max_retries()` |
| Timeout (default 120 s) | `timeoutMs` | `timeout` (seconds) | `.timeout(Duration)` |
| Custom HTTP | `fetch` | `session` (`requests.Session`) | — |

## When to use the API directly

The clients return JSON-like rows. If you want **Arrow tables** (to write Parquet, or to load
into pandas, Polars or DuckDB without conversion), or you use **another language**, read the
Arrow response yourself: [Reading Arrow](https://docs.bloklake.com/responses/reading-arrow.md) has complete readers, and
[Export to Parquet](https://docs.bloklake.com/guides/export-to-parquet.md) shows the pattern.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.