The Robinhood Chain sequencer feed
FirstLight serves the Robinhood Chain sequencer feed as a standard Arbitrum Nitro feed. A Nitro node reads it with one flag. Every entry is the sequencer's own bytes, so signatureV2 verifies unchanged.
Plan
The feed is the Pro plan. Ask for a key on Telegram.
Endpoint
wss://blocks.first-light.stream/{KEY}The key is the whole path. The relay ignores a query string. The key must name the feed (the blocks stream). See keys.
Connect
A Nitro node
Add this flag to your node's existing configuration. You need no other feed flag.
nitro ... --node.feed.input.url wss://blocks.first-light.stream/{KEY}The node's defaults for the feed timeout, the requested sequence number, compression and the signature checks all work with FirstLight. The relay sends Arbitrum-Chain-Id in its handshake. A node whose --chain.id is not Robinhood Chain's (4663) stops, by its own rule.
websocat
websocat 'wss://blocks.first-light.stream/{KEY}'Each line of output is one feed message as JSON.
Node.js
This example uses the ws package. It offers permessage-deflate, so it gets compressed frames, and it answers the relay's pings.
import WebSocket from "ws";
const ws = new WebSocket(`wss://blocks.first-light.stream/${process.env.KEY}`, {
perMessageDeflate: true,
});
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
for (const m of msg.messages ?? []) {
console.log(m.sequenceNumber, m.blockHash);
}
});
ws.on("close", (code) => console.log("closed", code));Messages
Each WebSocket message is a Nitro broadcast message, JSON. Each entry in it is the sequencer's own bytes. This is a real message, with the transaction bytes shortened:
{"version":1,"messages":[{"sequenceNumber":67026113,
"message":{"message":{"header":{"kind":3,
"sender":"0xa4b000000000000000000073657175656e636572",
"blockNumber":26011090,"timestamp":1789815234,
"requestId":null,"baseFeeL1":0},
"l2Msg":"..."},
"delayedMessagesRead":298770},
"blockHash":"0xb27d97bc733c49f0d00a12cfc23787b510251ded6593972b6de3959fbde9f3ce",
"signatureV2":"4TfOdtJgzDyi2/Ze2mic...",
"blockMetadata":null}]}| Field | Meaning |
|---|---|
sequenceNumber | The message's position in the sequencer's stream. One entry is one L2 block. |
message.message.header | The L1 message header: kind (3 is an L2 message), sender, blockNumber (the L1 block number), timestamp (whole seconds), requestId, baseFeeL1. |
message.message.l2Msg | The L2 message, base64. For kind 3 it holds the block's signed transactions. |
message.delayedMessagesRead | Nitro's count of delayed messages read, as the sequencer sent it. |
blockHash | The block hash that the sequencer claims. |
signatureV2 | The sequencer's signature over the entry. |
FirstLight forwards each entry byte for byte. It does not re-serialise or re-sign an entry. signatureV2 therefore verifies against message.SignatureHash(chainId) at your client.
Handshake
- The
101answer carriesArbitrum-Feed-Server-Version: 2andArbitrum-Chain-Id. - A new connection starts at the live head. The relay reads
Arbitrum-Requested-Sequence-Number. Where the relay keeps recent messages for catch-up and still holds the requested sequence, it serves you from that message. In all other cases you start at the live head. - A Nitro node that reconnects fills the messages that it missed from its parent chain.
Compression
The relay answers the standard permessage-deflate extension (RFC 7692) on this endpoint. A client that offers it gets compressed frames, about a quarter of the bytes.
- The answer is
permessage-deflate; server_no_context_takeover. It echoesclient_no_context_takeoverandserver_max_window_bits=15where your offer named them. An offer of a window smaller than 15 bits is not answered. - A compressed message can come in several frames, with RSV1 set on the first frame only. Join the frames of a message, then inflate the whole message with a fresh window. A message whose first frame has RSV1 clear is plaintext. Both can come on one connection. A standard WebSocket client does this for you.
- A Nitro node offers only
Arbitrum-permessage-deflate. The relay does not answer that offer, so a Nitro node gets plaintext. The node logs a warning and continues. - A client that offers nothing gets plaintext.
Limits
- Each key may hold a fixed number of connections on each stream. Your FirstLight contact tells you yours. One connection more takes the place of your oldest connection, which closes with code 4001.
- A key may take the place of its own connections six times in 60 s. The next attempt gets HTTP 429, and your oldest connection stays open.
- The relay pings each connection every 5 s. It closes a connection that it has not heard from for 15 s. Answer the pings: most WebSocket clients do this for you.
- The TLS handshake and the request must complete within 3 s. One address may have two connections in the handshake at a time.
- A client that does not read fast enough is closed with code 1013. Reconnect.
Errors and close codes
Refusals
A refusal has no body.
| Status | Meaning |
|---|---|
| 404 | The host is not a FirstLight stream, or the path is not exactly / and a key. |
| 401 | The key is unknown or expired. |
| 403 | The key does not name this stream. |
| 429 | The key took the place of its own connections too often. Wait, then reconnect. |
| 503 | The relay is full. Retry. |
Close codes
| Code | Meaning | What to do |
|---|---|---|
| 1011 | The relay could not complete the message in progress, or the relay ended the connection without a restart notice. | Reconnect. |
| 1012 | Service restart: we release a new relay. | Reconnect at once. |
| 1013 | Your client did not read fast enough. | Reconnect, and read faster. |
| 4001 | A newer connection of your key took this connection's place. | Check that only one client uses the key. |
| 4002 | Your key was kicked. | Ask your FirstLight contact. |
| 4003 | Your key expired. | Ask your FirstLight contact. |
| 4004 | Your key was revoked, or no longer names this stream. | Ask your FirstLight contact. |
The plain URL
On Ultra, a reader inside AWS us-east-2 can ask its FirstLight contact for a plain ws:// URL. It takes the same key and gives the same answers, without the cost of TLS.
On the plain URL your key travels in cleartext. Anyone on the path can read your key and use it until we revoke it. wss:// stays the default.