Solana Pre-Execution Transaction Stream
Transactions as the validator reconstructs them from the leader's shreds — before they execute. You see the intent before the network sees the result.
| Method / Endpoint | WS /ws/sol-preexec |
| Type | Triport product (WebSocket) |
| Network | Solana |
| Authentication | Authorization: Bearer $TRIPORT_API_KEY (or ?api-key= query param) |
| Required scope | sol.preexec on the API key (* wildcard keys also work) |
| Tier / rate limit | Pro tier and above; concurrent-stream capped, account filter required on Pro |
What This Is
A Solana validator reassembles transactions from the shreds it receives from
the slot leader. That reassembly happens before the runtime executes
anything. /ws/sol-preexec streams those reassembled transactions: signature,
the full raw transaction, and the addresses resolved from address lookup
tables.
Triport holds one or two upstream subscriptions to validators that expose this data, de-duplicates by signature across them, filters server-side against your account list, and fans the result out to you. You pay for one WebSocket instead of running your own multi-source geyser client.
What you do not get, by construction. These transactions have not executed. There is no status, no error, no logs, no inner instructions, no pre/post balances and no compute-unit accounting — that information does not exist yet at the moment the frame is produced. A transaction in this stream may fail, may be dropped, and the slot itself may be skipped. Confirm outcomes through a normal commitment channel such as
/ws/sol-stream.
How early is it?
Measured 2026-07-31 from the US PoP over 18,040 matched signatures, against
Triport's own /ws/sol-stream:
| Median | 15 ms earlier |
| 10th percentile | 39 ms earlier |
| Arrived first | 99.2% of transactions |
Against confirmed-commitment consumers the gap is much larger — roughly
0.9 of a slot (~360 ms) — because confirmed waits for a vote round that
this stream does not.
Read those two numbers precisely. The 15 ms figure is against a very fast
processed-level firehose, and it is consistent rather than dramatic: the
value here is that it wins almost every race, not that it wins by a lot. The
360 ms figure applies only if you are currently consuming confirmed.
Related channels
/ws/sol-stream | /ws/sol-firstshred | this channel | |
|---|---|---|---|
| Level | account/tx/slot/block, after execution | slot-level | transaction-level, before execution |
| Payload | full update with execution metadata | {slot, ts, parent} | signature + raw tx + ALT addresses |
| Answers | "what actually executed" | "a slot started forming" | "which transactions are in this slot" |
| Guarantees | executed at your chosen commitment | none | none |
Endpoint
wss://triport.io/ws/sol-preexec?accounts=<pubkey>,<pubkey>Query parameters
| Name | Required | Description |
|---|---|---|
accounts | yes on Pro | Comma-separated base58 pubkeys. A frame is delivered when the transaction touches any of them, including ALT-resolved addresses. Pro allows up to 50; Business up to 200. |
fmt | no | json (default) or raw — see Frame format. |
The filter is fixed for the lifetime of the connection. There is no in-stream filter replacement: the upstream subscription is shared across all subscribers, so a per-client filter swap would either rebuild it on every change or silently diverge from what you asked for. To change your filter, reconnect.
Why the filter is mandatory on Pro. Unfiltered, this stream is roughly
21.75 Mbit/s sustained. That is not a policy preference — it is more bandwidth
per subscriber than the Pro plan costs. Business may connect without a filter;
Pro connections without accounts are refused with close code 4003
(filter_required).
Authentication
wscat -c "wss://triport.io/ws/sol-preexec?accounts=$POOL" \
-H "Authorization: Bearer $TRIPORT_API_KEY"
# or
wscat -c "wss://triport.io/ws/sol-preexec?accounts=$POOL&api-key=$TRIPORT_API_KEY"Close codes: 4001 missing/invalid credentials; 4003 forbidden — carrying
tier_insufficient (below Pro), filter_required (empty filter on Pro) or
filter_too_wide (more accounts than the tier allows); 4029 you already hold
the maximum number of concurrent streams for your tier. See
Close codes for the shared set.
Frame format
Default (fmt=json), one object per transaction:
{
"signature": "5ggXGeZKdiqtr5zYSSWpvdW7MmB38P1JXwnoy1L3Dod1u2Naw9Xn3hNNpGa9cQNttiqcVPqBfYaaLuDM7SUTQbqB",
"slot": 436349534,
"is_vote": false,
"tx_pb_base64": "CkAB…",
"account_keys": ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", "…"],
"pre_exec": true,
"ts": 1751490000123
}| Field | Type | Meaning |
|---|---|---|
signature | string | base58 transaction signature. |
slot | uint64 | Slot the transaction was reconstructed in. |
is_vote | bool | Vote transaction flag. |
tx_pb_base64 | string | The transaction, protobuf-encoded — see the warning below. |
account_keys | string[] | Static message keys first, then ALT-resolved writable, then ALT-resolved readonly, in the order instructions index them. |
pre_exec | bool | Always true. Present so that code which mixes this stream with executed-transaction feeds cannot confuse the two. |
ts | int64 | Unix milliseconds when Triport received the upstream frame. |
tx_pb_base64is not a wire transaction. It is base64 of the protobuf encoding (solana.storage.ConfirmedBlock.Transaction), not Solana's canonical bincode wire format. You cannot pass it tosendTransactionwithout re-encoding. The field is namedtx_pb_base64rather thantx_base64specifically so this is impossible to miss.
With fmt=raw the server forwards the upstream protobuf message unchanged.
This is cheaper on both sides, but it binds your client to the upstream schema —
prefer JSON unless you have a reason.
Delivery semantics
- At-most-once, no replay. Live stream only; on a drop, reconnect and resume from the current moment.
- De-duplicated by signature across upstream sources, within a 5-second window. Sources disagree by tens to hundreds of milliseconds, so this covers the skew with a wide margin — but treat your consumer as idempotent by signature anyway.
- Multi-source by default. Individual validators episodically stall for seconds. A second source covers the gap without interrupting your stream; you do not see the failover.
- Best-effort per client. If you stop reading and your buffer fills, the server drops frames for you rather than blocking other subscribers. Under backpressure this stream is sampled, not queued.
Example (Node.js)
import WebSocket from "ws";
const pools = ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"];
const ws = new WebSocket(
`wss://triport.io/ws/sol-preexec?accounts=${pools.join(",")}`,
{ headers: { Authorization: `Bearer ${process.env.TRIPORT_API_KEY}` } },
);
ws.on("message", (buf) => {
const tx = JSON.parse(buf.toString());
// tx.pre_exec === true: this has NOT executed yet.
// React on intent, then confirm the outcome elsewhere before treating it as fact.
console.log(`pre-exec ${tx.signature} slot=${tx.slot}`);
});
ws.on("close", (code) => {
// 4001 unauthorized · 4003 forbidden (tier / filter) · 4029 stream limit
console.error("closed", code);
});Notes
- Pro tier. Gated on the
sol_preexectier feature, available from Pro upward. Business additionally may connect without a filter. - Pair with confirmation. This is an early trigger, not a guarantee. Any action taken on it should be reconciled against a commitment channel.
- See Streaming overview, Authentication, and Rate limits & tiers for account-wide details.