FirstLight Submit
FirstLight Submit is Robinhood Chain transaction submission at one URL: it sends your signed transactions to the sequencer. It takes the standard JSON-RPC call eth_sendRawTransaction. You change one URL in your client.
Plan
Submit is part of the Ultra plan. Your FirstLight contact enables submit on your key and tells you its limits. Ask for a key on Telegram.
Endpoint
https://submit.first-light.stream/{KEY}{KEY} is the key that you hold for the FirstLight streams. You can also send the key in a header, with the path /:
POST https://submit.first-light.stream/
Authorization: Bearer {KEY}Submit runs in AWS Ohio (us-east-2), in the sequencer's AWS zone. It speaks HTTP/1.1 with keep-alive. It does not speak HTTP/2.
The plain URL
A client inside AWS us-east-2 can ask its FirstLight contact for a plain http:// URL. It takes the same key and gives the same answers, without the cost of TLS.
On the plain URL your key and your transactions travel in cleartext. Anyone on the path can read your key and use it until we revoke it. https:// stays the default.
The calls
| Method | Answer |
|---|---|
eth_sendRawTransaction | One call, or a JSON-RPC batch of calls. |
eth_chainId | 0x1237 (chain ID 4663). Submit answers it directly, and it sends nothing to the sequencer. |
| Any other method | HTTP 200 with the JSON-RPC error -32601, "the method <name> does not exist/is not available". |
Read your nonce, gas and receipts from the chain's public RPC. Sign the transaction, then send the signed bytes to Submit. Submit does not check a nonce or a signature. The sequencer checks them.
The answers
The default answer
By default you get the answer for your transaction verbatim: the transaction hash, or the error. This answer comes after the sequencer has sequenced the transaction.
The fast answer
Send the header Rhc-Answer: hash, or ask your FirstLight contact to set the fast answer on your key. Submit then answers the keccak-256 hash of your transaction as soon as it has sent the transaction. The response header Rhc-Written-Us gives the time from your request to that send, in microseconds. With the fast answer, an error from the sequencer does not reach you. Read the receipt to know the result.
Limits
Each key has four limits: a rate per second, a burst, a number of transactions in flight and a number of connections. The defaults are 5 per second, a burst of 20, 10 in flight and 4 connections. Your FirstLight contact tells you the limits of your key and can raise them.
- A batch of n calls takes n from the rate. Submit admits a batch whole or refuses it whole, so a refusal never leaves a gap in your nonces.
- A transaction stays in flight until Submit has the sequencer's answer, also after a fast answer.
- A request body larger than 384 KiB gets HTTP 413.
- A signed transaction larger than 128 KiB (131,072 bytes) is refused with the JSON-RPC error
-32602.
Send the key in the path or in the Authorization header, not in both. A request with a key in the path and an Authorization header gets 404.
Errors
| Answer | Meaning | Connection |
|---|---|---|
429, Retry-After: 1, JSON-RPC -32005 "limit exceeded: rate" | Over the rate or the burst. | Stays open |
429, -32005 "limit exceeded: inflight" | Too many transactions in flight. | Stays open |
429, -32005 "limit exceeded: blocked" | The sequencer refused more than half of your last 64 transactions. The block lasts 60 s. | Stays open |
429, -32005 "limit exceeded: connections" | Too many connections for your key. | Closed after this answer |
503, -32005 "no warm connection" | Submit had no ready connection to the sequencer for your transaction. Retry after a short wait. | Stays open |
503, -32005 "submission is off" | We stopped submission. Retry after a short wait. | Stays open |
| 404, no body | The path is not a key. | Closed |
| 401, no body | The key is unknown or expired. | Closed |
| 403, no body | The key does not have submit, or a second key was sent on the same connection. | Closed |
| 413 | The request body is larger than 384 KiB. |
Keep your connection warm
A new connection costs two round trips (TCP and TLS 1.3). A warm connection costs neither.
- Submit never closes an idle connection. Each answer carries
Keep-Alive: timeout=600andConnection: keep-alive. Submit closes a connection that takes no byte of an answer for 30 s. - Node's undici uses that hint, up to its own maximum of 600 s. Go
net/httpand reqwest close an idle connection after 90 s by default. Set their idle timeout higher, as the examples below do. - Python
requestsnever closes an idle connection of aSession. Send through oneSession. - When you are idle for minutes, send
eth_chainId. It keeps the connection open and sends nothing to the sequencer. - HTTP/1.1 holds one request in flight per connection. For a burst, open connections up to your connection limit and keep them open. Above that limit, send batches or use the fast answer.
Examples
In these examples, URL is https://submit.first-light.stream/{KEY} and raw is the signed transaction as 0x hex.
curl
curl -sS -D - "$URL" -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0x02f8..."]}'
# Add -H 'Rhc-Answer: hash' for the fast answer.Node.js with undici
import { Agent, request } from "undici";
const agent = new Agent({ connections: 4, keepAliveTimeout: 600_000, keepAliveMaxTimeout: 600_000 });
const { statusCode, body } = await request(URL, {
method: "POST", dispatcher: agent, headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_sendRawTransaction", params: [raw] }),
});
const answer = await body.json();ethers v6
import { JsonRpcProvider } from "ethers";
const submit = new JsonRpcProvider(URL, undefined, { staticNetwork: true, batchMaxCount: 1 });
const hash = await submit.send("eth_sendRawTransaction", [raw]);Do not use broadcastTransaction here. It also calls eth_blockNumber, which Submit does not serve. By default ethers groups the calls of 10 ms into one batch, and Submit admits or refuses a batch whole. batchMaxCount: 1 sends each call alone.
viem
import { createWalletClient, http } from "viem";
const submit = createWalletClient({ transport: http(URL) });
const hash = await submit.sendRawTransaction({ serializedTransaction: raw });Gas on Robinhood Chain
The intrinsic gas of a transfer changes with the cost of L1 data. The sequencer refuses a gas limit of 21,000 as "intrinsic gas too low". Set the gas limit at or above eth_estimateGas from the public RPC.
"Already known" and nonce order
- The sequencer can answer "already known" or "nonce too low" for a transaction that it has accepted. You pay gas once. With the default answer, you get the answer for the accepted transaction.
- Two transactions with dependent nonces, sent within about a millisecond, can reach the sequencer out of order. The sequencer then refuses the later nonce. Send a dependent transaction after the answer to the one before it, or be ready to send it again.
What we see
Per key, we see the transactions accepted, the transactions that landed on the chain, the transactions limited or refused and why, the sequencer's errors, and the time from your request to the first send. We do not record your key or your URL. We name a key by a short ID.