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

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}]}
FieldMeaning
sequenceNumberThe message's position in the sequencer's stream. One entry is one L2 block.
message.message.headerThe L1 message header: kind (3 is an L2 message), sender, blockNumber (the L1 block number), timestamp (whole seconds), requestId, baseFeeL1.
message.message.l2MsgThe L2 message, base64. For kind 3 it holds the block's signed transactions.
message.delayedMessagesReadNitro's count of delayed messages read, as the sequencer sent it.
blockHashThe block hash that the sequencer claims.
signatureV2The 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 101 answer carries Arbitrum-Feed-Server-Version: 2 and Arbitrum-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 echoes client_no_context_takeover and server_max_window_bits=15 where 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.

StatusMeaning
404The host is not a FirstLight stream, or the path is not exactly / and a key.
401The key is unknown or expired.
403The key does not name this stream.
429The key took the place of its own connections too often. Wait, then reconnect.
503The relay is full. Retry.

Close codes

CodeMeaningWhat to do
1011The relay could not complete the message in progress, or the relay ended the connection without a restart notice.Reconnect.
1012Service restart: we release a new relay.Reconnect at once.
1013Your client did not read fast enough.Reconnect, and read faster.
4001A newer connection of your key took this connection's place.Check that only one client uses the key.
4002Your key was kicked.Ask your FirstLight contact.
4003Your key expired.Ask your FirstLight contact.
4004Your 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.

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