New: the Free plan. Check FirstLight's timing against your own feed
All docs pages

Raw transactions on /tx

The /tx stream sends each signed Robinhood Chain transaction as one binary record, as soon as its bytes complete, usually before its block completes. You need no node and no decoder of your own.

Plan

/tx is part of the Ultra plan. Ask for a key on Telegram.

Endpoint

wss://tx.first-light.stream/{KEY}

The host selects the stream, and the key is the whole path. The key must name the tx stream. See keys.

How a block reaches you

On the sequencer feed, a block is an entry: a sequence number, a timestamp, the block hash that the sequencer claims, and an ordered list of signed transactions.

  1. For each transaction that completes early, you get a Transaction record with the provisional flag. Ordinals start at 0 and increase by one.
  2. When the entry is complete, you get a Transaction record without the provisional flag for each transaction that the provisional records did not cover.
  3. Then you get exactly one EntryComplete record for the sequence. It confirms the provisional records. Its tx_count is the number of transactions in the block.

The record

Each record is one binary WebSocket frame. A frame is never fragmented. The relay never sends a text frame on /tx. All integers are little-endian. Each record has a 28-byte header, then the payload.

OffsetTypeFieldMeaning
0u8version1
1u8kind1 Transaction, 2 EntryComplete, 3 Dropped, 4 Gap
2u8flagsbit 0: provisional. bit 1: hash present. Bits 2 to 7 are zero.
3u8tx_typeKind 1: 0 for a legacy transaction, else the EIP-2718 type byte. Else 0.
4u32ordinalKind 1: the transaction's index in the block. Else 0.
8u64sequenceThe feed's sequence number. Kind 4: the first sequence after the gap.
16u64received_unix_nsThe relay's stamp, in nanoseconds, of the read that completed these bytes (kinds 1 and 2). 0 for kinds 3 and 4.
24u32payload_lenThe length of the payload in bytes.

Payloads

Kind 1, Transaction
The exact signed transaction bytes (the EIP-2718 envelope, or the RLP list of a legacy transaction). keccak-256 of these bytes is the transaction hash. If flag bit 1 is set, the 32 bytes of the hash follow, inside payload_len.
Kind 2, EntryComplete
44 bytes: u32 tx_count, u64 sequencer_timestamp (whole seconds), [u8; 32] block_hash. A block hash of 32 zero bytes means that the sequencer sent none.
Kind 3, Dropped
1 byte: u8 reason. 1: the entry was malformed. 2: a later entry, a gap or a malformed entry passed it. 3: the relay abandoned it. Discard the provisional transactions of the sequence.
Kind 4, Gap
8 bytes: u64 expected. The relay never saw the sequences from expected up to, but not including, sequence.

What you can rely on

  • Within a sequence, the order is: provisional Transaction records, then the other Transaction records, then one EntryComplete.
  • Records of different sequences can interleave, because more than one sequence can be open at a time.
  • A Dropped comes only for a sequence whose provisional records you received. Any reason means: discard them. After reason 3, the entry can still arrive, and then it arrives whole: all its transactions again from ordinal 0, without the provisional flag, then EntryComplete. Count only the Transaction records after the Dropped against tx_count.
  • An open sequence below a Gap gets its Dropped (reason 2) before the Gap record.
  • EntryComplete sequences increase. They are not always consecutive.
  • A new connection receives only sequences that start after it connected. It never joins in the middle of a sequence.
  • tx_count counts the signed transactions that the entry carried. It does not count what the chain executed.
  • The stream has no history and no filters, and it does not recover the sender. To get from, recover it from the signature.

Request and response headers

  • Send X-Rhc-Hash: 1 on the upgrade request to get the hash on every Transaction record (flag bit 1).
  • The 101 answer carries X-Rhc-Record-Version: 1. Check it, and check byte 0 of each record. Refuse a version that you do not know.
  • The 101 answer carries X-Rhc-Relay-Epoch, which identifies the relay process. Two received_unix_ns stamps of one epoch subtract to an interval. After a relay restart, sequences can repeat: deduplicate by sequence.

Versions

  • A frame is one record. Its length is 28 + payload_len.
  • Skip a kind that you do not know, by payload_len.
  • A flag that you did not ask for and do not know is a protocol error.
  • A change to the header or to an existing payload would be version 2. A version 2 would be served beside version 1, and we would tell you its URL.

Connect

websocat

websocat -b -H 'X-Rhc-Hash: 1' 'wss://tx.first-light.stream/{KEY}' | xxd | head

Node.js

import WebSocket from "ws";

const ws = new WebSocket(`wss://tx.first-light.stream/${process.env.KEY}`, {
  headers: { "X-Rhc-Hash": "1" },
});
ws.on("upgrade", (res) => {
  if (res.headers["x-rhc-record-version"] !== "1") throw new Error("not /tx version 1");
});
ws.on("message", (b, isBinary) => {
  if (!isBinary || b[0] !== 1) return;
  const kind = b[1], flags = b[2];
  const sequence = b.readBigUInt64LE(8);
  const payload = b.subarray(28, 28 + b.readUInt32LE(24));
  if (kind === 1) {
    const ordinal = b.readUInt32LE(4);
    const provisional = (flags & 1) === 1;
    const tx = flags & 2 ? payload.subarray(0, payload.length - 32) : payload;
    const hash = flags & 2 ? payload.subarray(payload.length - 32) : null;
    console.log("tx", sequence, ordinal, provisional, hash?.toString("hex"), tx.length);
  } else if (kind === 2) {
    console.log("complete", sequence, payload.readUInt32LE(0), "transactions");
  } else if (kind === 3) {
    console.log("dropped", sequence, "reason", payload[0]);
  } else if (kind === 4) {
    console.log("gap", payload.readBigUInt64LE(0), "to", sequence);
  }
});

The ws package answers the relay's pings for you. A client that reads frames by hand must answer each ping with a masked pong.

Limits and errors

The /tx stream has the same refusals, connection limits, pings and close codes as the feed. It does not negotiate permessage-deflate. On any reconnect, discard each sequence that is still open: the new connection does not tell you more of it.

Need a key? Ask on TelegramMessage @idatsy, direct to the operator. Start with a free key for the headers stream.