> For the complete documentation index, see [llms.txt](https://docs.triton.one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.triton.one/chains/solana/preconfirmations-grpc.md).

# Preconfirmations gRPC

Triton Preconfs streams preconfirmed Solana transactions over gRPC, from the two block production systems that emit them: Harmonic and BAM.

#### What a preconfirmation is

On Solana one validator is the [leader](https://docs.anza.xyz/consensus/leader-rotation) for each [slot](https://solana.com/docs/references/terminology#slot) and builds the block for it. A transaction you send travels to that leader; the leader executes it, packs it into entries, splits the entries into shreds and broadcasts them; every other node receives the shreds, replays the block and only then reports the transaction, first at `processed` [commitment](https://solana.com/docs/rpc#configuring-state-commitment), later at `confirmed` and `finalized`. Everything you can observe through an RPC node, a WebSocket or a Geyser stream sits at the end of that path.

A preconfirmation is a message emitted at the start of it. The party assembling the block, the builder or the leader itself, tells you the moment a transaction has been executed or committed into the slot under construction, before any shred exists. You learn what the block will contain while it is still being built. The message carries the raw transaction bytes exactly as the block will, plus the slot and, depending on the feed, the execution outcome and the position in the block.

A preconfirmation is a statement by the block producer, not a confirmation by the cluster. The slot can still be skipped, the block can land on a fork that is abandoned, and a builder can restart a slot. A preconfirmed transaction almost always lands, but "almost" is the operative word: act on it as the earliest possible signal, and treat `confirmed` and `finalized` as the settlement they are.

#### How each feed produces one

[**Harmonic**](https://docs.harmonic.gg/) runs block builders next to leaders that run the Harmonic validator client. Transactions reach the builder, which executes them and fixes their order for the block. As it does so it streams the executed transactions to subscribers in batches, numbered from zero within each slot and delivered in order, with a start and an end marker per slot. Each transaction carries the outcome the builder observed: success, execution failure (committed, fees charged, state reverted) or fees only (failed to load, only the fee is charged). The leader then executes the same transactions in the same order; Harmonic guarantees the resulting state is equivalent to executing them serially in batch order, so the outcome you receive is the outcome that lands. A Harmonic preconfirmation therefore means: this transaction was executed with this result and sits in the block being built.

[**BAM**](https://bam.dev/docs/bam/bam-overview/), Jito's Block Assembly Marketplace, sits between transaction senders and leaders that run the BAM validator client. A BAM node schedules the transactions it receives, first in first out with respect to the accounts they lock, and hands them to the leader; the leader executes them and acknowledges each one back to the node, which streams it to subscribers. Each transaction carries the node it came through, its position in the scheduler's sequence (transactions sharing a sequence were bundled together), its position inside that bundle, and whether the bundle reverts on error. BAM does not report an execution outcome: a BAM preconfirmation means this transaction was committed by the leader into the slot; whether it succeeded is only visible once the block lands.

Triton subscribes to both systems in every region they publish from and delivers the same bytes to you over one endpoint, filtered to the accounts and signatures you ask for.

#### Harmonic and BAM side by side

|                          | Harmonic                                             | BAM                                                      |
| ------------------------ | ---------------------------------------------------- | -------------------------------------------------------- |
| who produces the preconf | the block builder, before the leader executes        | the BAM node, after the leader executes and acknowledges |
| what it asserts          | executed with this outcome, in the block being built | committed by the leader into the slot                    |
| execution outcome        | success, execution failure, fees only                | not reported                                             |
| ordering information     | batch number within the slot                         | scheduler sequence, bundle position, revert on error     |
| slot framing             | `SlotStart` and `SlotEnd` per slot                   | none, each transaction names its slot                    |
| regions                  | 7                                                    | 15                                                       |

#### Endpoint

`https://preconfs.rpcpool.com`

The address is anycast: the connection lands on the closest point of presence, one of the servers behind that address; every point of presence serves every region of both feeds. Every request carries an `x-token`, the token issued with your preconfs subscription.

Each account may receive up to a share of a feed's traffic. Over it, matching transactions are withheld and the count is announced on the stream; nothing is ever dropped silently. See The stream.

#### Client

The Rust client and the protobuf definitions are on [GitHub](https://github.com/rpcpool/preconfs-client) as the `triton-preconfs-client` and `triton-preconfs-proto` crates. Other languages use the proto directly, see Other languages.

### Quick start

Add the client:

```toml
[dependencies]
triton-preconfs-client = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

Subscribe to Harmonic in Amsterdam for transactions touching the SPL token program and print what arrives:

```rust
use solana_pubkey::Pubkey;
use triton_preconfs_client::{Connector, Event, Feed, Filter, Filters, Region, parse};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Connector::new("https://preconfs.rpcpool.com")
        .x_token(Some(std::env::var("PRECONFS_TOKEN")?))
        .connect()
        .await?;

    let token_program: Pubkey = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".parse()?;
    let region = Region::parse(Feed::Harmonic, "ams")?;
    let filters = Filters::single(Filter::new().accounts([token_program]));

    let mut stream = client.subscribe_harmonic(region, filters).await?;
    while let Some(event) = stream.next().await {
        match event? {
            Event::Transaction(matched) => {
                let signature = parse::parse_signature(&matched.transaction.transaction)?;
                println!("slot {} {signature}", matched.transaction.slot);
            }
            Event::SlotEnd { slot } => println!("slot {slot} complete"),
            Event::Reconnected { attempts } => println!("reconnected after {attempts} attempts"),
            _ => {}
        }
    }
    Ok(())
}
```

The same shape works for BAM with `Feed::Bam` and `subscribe_bam`.

#### The example CLI

The repository ships `preconfs-subscribe`, which subscribes and logs every event:

```
cargo run -p preconfs-example -- --x-token $TOKEN --region harmonic:ams \
    --account TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
```

`--region bam:fra`, `--require`, `--signature`, `--result` and `--no-reconnect` cover the other options; `--help` lists them.

### Feeds and regions

Three things carry the word region; they are not the same thing.

```
Solana leaders                        Solana leaders
      │                                     │
      ▼                                     ▼
Harmonic builders                       BAM nodes
ams ewr fra lon tyo sgp slc             ams dfw dub ewr fra hkg iad lax
                                        lon pit sea sin slc sqq tyo
      │                                     │
      └─────────────────┬───────────────────┘
                        ▼
          Triton points of presence
          ams1 fra1 lon1 nyc1 ...   each relays every region
                        │
                        ▼
          https://preconfs.rpcpool.com   anycast picks the closest
                        │
                        ▼
          your stream: one feed, one region
```

**Feed regions** are the ones in the proto (`HarmonicRegion`, `BamRegion`) and in the subscribe request. Harmonic runs a block builder in each of its regions and BAM runs a node in each of its; a leader is served by one of them, and that location's stream carries the slots of the leaders it serves. The regional streams are not copies of each other: `harmonic:ams` and `harmonic:fra` deliver different slots. To see every preconfirmed slot of a feed, subscribe to each of its regions, one stream per region.

**Origin on each transaction**: `region` on a Harmonic transaction and `node` on a BAM transaction name the builder or node it came through. On a single region stream it matches what you subscribed; it is there so transactions stay self describing when you merge streams.

**Points of presence** are Triton's servers, named after their site (`ams1`, `fra1`, `lon1`, `nyc1` and so on). The anycast address lands you on the closest one and every point of presence relays every feed region, so where you connect never limits what you can subscribe to. `Client::version` returns the one that answered in its `region` field.

A stream serves one feed in one region. A region of the other feed is refused.

#### Harmonic

The builder executes the transaction and reports the outcome. Streams are framed per slot (see The stream) and carry an execution result on every transaction, so `execution_results` filters are accepted.

| region | location          |
| ------ | ----------------- |
| `ams`  | Amsterdam         |
| `ewr`  | New York (Newark) |
| `fra`  | Frankfurt         |
| `lon`  | London            |
| `tyo`  | Tokyo             |
| `sgp`  | Singapore         |
| `slc`  | Salt Lake City    |

#### BAM

The leader commits the transaction; no outcome is reported and there is no slot framing. Each transaction names its slot.

| region | location             |
| ------ | -------------------- |
| `ams`  | Amsterdam            |
| `dfw`  | Dallas               |
| `dub`  | Dublin               |
| `ewr`  | New York (Newark)    |
| `fra`  | Frankfurt            |
| `hkg`  | Hong Kong            |
| `iad`  | Washington (Ashburn) |
| `lax`  | Los Angeles          |
| `lon`  | London               |
| `pit`  | Pittsburgh           |
| `sea`  | Seattle              |
| `sin`  | Singapore            |
| `slc`  | Salt Lake City       |
| `sqq`  | Siauliai             |
| `tyo`  | Tokyo                |

#### In the client

```rust
let region = Region::parse(Feed::Harmonic, "ams")?;
let region: Region = "bam:fra".parse()?;
Feed::Bam.regions(); // the names above
```

To pin one point of presence instead of letting anycast choose, `Connector::dial("host:port")` opens the TCP connection to that address while TLS keeps the endpoint's host name. `Client::version` returns the server version and the region of the point of presence answering.

### Filters

A subscribe request carries one or more named filters. A transaction is delivered when it matches at least one, and every delivered transaction names the filters it matched.

A transaction matches a filter when it satisfies every condition the filter sets:

| condition           | matches when the transaction                     |
| ------------------- | ------------------------------------------------ |
| `account_include`   | references any of these accounts                 |
| `account_required`  | references all of these accounts                 |
| `signatures`        | has one of these first signatures                |
| `execution_results` | landed with one of these outcomes, Harmonic only |

Every filter must set at least one of the first three; the full feed cannot be subscribed.

#### Limits

| limit                                                     | value    |
| --------------------------------------------------------- | -------- |
| filters per stream                                        | 64       |
| accounts per `account_include` or `account_required` list | 10000    |
| signatures per filter                                     | 1000     |
| filter name                                               | 64 bytes |

The client checks these before sending, so a request over a limit fails locally with a `FilterError`.

#### In the client

```rust
let filters = Filters::new()
    .with("token", Filter::new().accounts([token_program]))
    .with("mine", Filter::new().accounts([token_program]).require([my_account]))
    .with("landed", Filter::new().accounts([my_account]).execution_results([ExecutionResult::Success]));
```

`Filters::single(filter)` names a lone filter `default`.

### The stream

`subscribe_harmonic` and `subscribe_bam` return a stream of `Event`s.

| event                      | meaning                                                                                                                     |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `SlotStart { slot }`       | a leader began streaming preconfs for this slot (Harmonic)                                                                  |
| `Transaction(matched)`     | a matching transaction; `matched.filters` names the filters, `matched.transaction` is the feed's message with the raw bytes |
| `SlotEnd { slot }`         | no further transactions for this slot will arrive (Harmonic)                                                                |
| `Clip { transactions }`    | matching transactions were withheld, see Coverage                                                                           |
| `Reconnected { attempts }` | the stream dropped and was resubscribed; the data produced in between is gone                                               |

Pings are consumed by the stream.

#### Slot framing

On Harmonic every transaction sits between its slot's `SlotStart` and `SlotEnd`. After `SlotEnd` for a slot you hold everything your filters matched for it. A stream that subscribes while a slot is open joins at the next `SlotStart`, so the first slot you see is always complete.

Rarely a leader restarts a slot: a second `SlotStart` for a slot that already ended, followed by its definitive transactions and a new `SlotEnd`. Rebuild your view of that slot from the new frame; the last `SlotEnd` wins.

BAM has no framing. Each transaction names its slot.

#### Transaction bytes

`matched.transaction.transaction` holds the raw transaction bytes. The client parses what filtering needs without a full decode:

```rust
let signature = parse::parse_signature(&bytes)?;
let (signature, account_keys) = parse::parse_static_parts(&bytes)?;
```

Legacy, v0 and v1 message formats are supported.

#### Transaction fields

Harmonic (`HarmonicTransaction`):

| field         | meaning                                                               |
| ------------- | --------------------------------------------------------------------- |
| `transaction` | raw transaction bytes                                                 |
| `slot`        | slot the transaction was preconfirmed in                              |
| `result`      | the builder's outcome: success, execution failure or fees only        |
| `region`      | Harmonic region it was received from                                  |
| `seq`         | preconf batch within the slot, numbered from 0 and delivered in order |

BAM (`BamTransaction`):

| field                | meaning                                                                             |
| -------------------- | ----------------------------------------------------------------------------------- |
| `transaction`        | raw transaction bytes                                                               |
| `slot`               | slot the transaction was preconfirmed in                                            |
| `node`               | BAM node it was received from                                                       |
| `sequence`           | scheduler order on that node; transactions sharing a sequence were bundled together |
| `bundle_position`    | execution order within that bundle                                                  |
| `is_revert_on_error` | the bundle reverts on error (bundle intent, not an outcome)                         |

#### Coverage

Each account may receive up to a share of a feed's traffic over a sliding window. Over it, matching transactions are withheld and the count arrives as `Clip`, always before the affected slot's `SlotEnd`. Staying over it ends the stream with `ResourceExhausted` and subscribing again is refused for a cooloff period; filters that select only what you need keep you under the share.

#### Slow consumers

A stream that cannot keep up ends with an explicit error status.

### Errors and reconnect

#### Error types

| type             | from                                  | when                                                                                                                                                    |
| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ConnectError`   | `Connector::connect`, `connect_lazy`  | bad endpoint URI, token not ascii, TLS setup, connection refused or timed out                                                                           |
| `SubscribeError` | `subscribe_harmonic`, `subscribe_bam` | region of the wrong feed, filters over a limit, or the server refused the subscribe (bad token, region not served, feed not entitled, too many streams) |
| `StreamError`    | a stream item                         | the server ended the stream with a status, or closed it                                                                                                 |

`RegionError`, `FilterError` and `ParseError` are the smaller types behind them. For one top level type wrap them with `anyhow` or `Box<dyn Error>`.

#### Reconnect

Points of presence restart on every deploy, so a long lived stream will drop. By default the stream resubscribes with the same request after a backoff and yields `Event::Reconnected`. Preconfs produced in between are gone; on Harmonic, framing restarts at the next `SlotStart`.

Errors that retrying cannot fix end the stream instead: `Unauthenticated`, `PermissionDenied`, `InvalidArgument`, `FailedPrecondition`, `NotFound`, `Unimplemented`. `Unavailable`, `DataLoss`, `ResourceExhausted`, `Internal`, `Aborted`, `DeadlineExceeded` and a closed stream are retried.

The default schedule waits 100ms, then doubles up to 10s between attempts, and never gives up. Tune or disable it on the connector:

```rust
use std::time::Duration;
use triton_preconfs_client::{Connector, Reconnect};

Connector::new(endpoint).reconnect(Reconnect {
    initial_interval: Duration::from_millis(250),
    multiplier: 2.0,
    max_interval: Duration::from_secs(30),
    max_retries: Some(20),
});

Connector::new(endpoint).no_reconnect();
```

With reconnect off the stream yields the error and ends.

### Other languages

The wire contract is `preconfs.proto` in the repository (`preconfs-proto/proto/preconfs.proto`); the same file is exported by the Rust proto crate as `PROTO_SOURCE`. Generate a client for your language from it.

#### Calls

| service                 | rpc                                                           |                                             |
| ----------------------- | ------------------------------------------------------------- | ------------------------------------------- |
| `preconfs.Harmonic`     | `Subscribe(SubscribeRequest) returns (stream HarmonicUpdate)` | the Harmonic stream                         |
| `preconfs.Harmonic`     | `GetVersion(VersionRequest) returns (VersionResponse)`        | server version and point of presence region |
| `preconfs.BAM`          | `Subscribe(SubscribeRequest) returns (stream BamUpdate)`      | the BAM stream                              |
| `preconfs.BAM`          | `GetVersion(VersionRequest) returns (VersionResponse)`        |                                             |
| `grpc.health.v1.Health` | `Check`                                                       | standard health service                     |

#### Authentication

Send the token as gRPC metadata: key `x-token`, value the token.

#### Request

`SubscribeRequest` has two fields:

* `transactions`: a map from filter name to `TransactionFilter` (`account_include`, `account_required`, `signature`, `execution_results`; accounts and signatures as base58 strings).
* `region`: exactly one of `harmonic_region` or `bam_region`, matching the service called. Unspecified or the other feed's region is `INVALID_ARGUMENT`; a region the server does not serve is `FAILED_PRECONDITION`.

The limits in Filters apply; a request over them is `INVALID_ARGUMENT`.

#### Updates

`HarmonicUpdate` and `BamUpdate` carry `filters` (the names that matched, empty for slot boundaries and pings) and one payload: `transaction`, `ping`, `clip`, and on Harmonic `slot_start` and `slot_end`. The contract in The stream holds as described; pings arrive on quiet streams and can be ignored.

Transactions carry the raw bytes in `transaction`. The first signature is the 64 bytes after the compact-u16 signature count.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.triton.one/chains/solana/preconfirmations-grpc.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
