Corvus Labs

Falcon native UDP

The exact Falcon UDP datagram frame for Solana transactions, every silent drop reason, and how to retry when nothing ever answers.

One signed Solana transaction per datagram, no envelope, and no response, for any reason. Everything you learn about the outcome, you learn from the signature on a normal Solana RPC.

You send to <region>.falcon.wtf:9000, so Frankfurt is fra.falcon.wtf:9000.

Build the frame

Falcon accepts two frame formats and tells them apart by shape. We recommend the binary one: it's 21 bytes smaller and can't be misread.

+----------------------+----------------------------------+
| 16-byte API key UUID | raw serialized transaction bytes |
+----------------------+----------------------------------+
   bytes 0..16            bytes 16..N
FieldSize
API keyExactly 16 bytes: the raw UUID, not the 36-character string
Transaction1 to 1,232 bytes

No length prefix, no version byte, no checksum, no base64, no delimiter.

ASCII frame (alternative)

+------------------------+-----------+----------------------------------+
| 36-char UUID text      | delimiter | raw serialized transaction bytes |
+------------------------+-----------+----------------------------------+
   bytes 0..36              byte 36     bytes 37..N

The delimiter is one of :, newline, space or tab.

That 1,232-byte cap applies to the transaction portion only; the UUID prefix and the delimiter don't count against it. So the largest legal datagram is 1,248 bytes binary, or 1,269 bytes ASCII.

Falcon only reads the ASCII form when all three hold: the datagram is longer than 36 bytes, byte 36 is a delimiter, and the first 36 bytes parse as a UUID. A binary frame that happens to carry a delimiter byte at offset 36 still parses correctly as binary.

Telemetry covers what not to record about these frames.

Send a datagram from Node.js

export FALCON_UDP_HOST='fra.falcon.wtf'
export FALCON_UDP_PORT='9000'
export FALCON_API_KEY='00000000-0000-0000-0000-000000000000'
import dgram from 'node:dgram';
import { readFile } from 'node:fs/promises';

const host = process.env.FALCON_UDP_HOST;
const port = Number(process.env.FALCON_UDP_PORT);
const apiKey = process.env.FALCON_API_KEY;

if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(apiKey ?? '')) {
  throw new Error('FALCON_API_KEY must be a UUID');
}

const apiKeyBytes = Buffer.from(apiKey.replaceAll('-', ''), 'hex'); // 16 bytes
const transactionBytes = await readFile('transaction.bin');

if (transactionBytes.length === 0 || transactionBytes.length > 1232) {
  throw new Error('Transaction must be 1 to 1232 bytes');
}

const packet = Buffer.concat([apiKeyBytes, transactionBytes]);
const socket = dgram.createSocket('udp4');

socket.send(packet, port, host, (error) => {
  socket.close();
  if (error) throw error;
});

The ASCII variant is the same code with a different prefix:

const packet = Buffer.concat([Buffer.from(`${apiKey}:`, 'ascii'), transactionBytes]);

Why a datagram is dropped

The reasons, in the order they're evaluated:

ReasonTrigger
Empty frameDatagram is empty, or carries a key and no transaction bytes (exactly 16 bytes binary, exactly 37 bytes ASCII)
Missing API keyDatagram shorter than 16 bytes
Too largeTransaction portion exceeds 1,232 bytes
Unauthorized keyThe key is not a live Falcon key. A malformed 36-character ASCII UUID lands here too rather than in its own reason: the datagram falls through to the binary reading, so its first 16 bytes become the key
Invalid transaction bytesWill not deserialize, unsigned, or signature count mismatch
Tip below minimumNo qualifying tip instruction, or below the minimum; see Tips
Forward failedFalcon accepted it but could not forward it; only ever visible as a signature that never lands

There's no rate-limit feedback on UDP either. Pace your sends to your allowance.

Every reason above except forward failed produces an explicit, readable error on the JSON-RPC route, so when UDP submissions vanish, replay one identical transaction to http://<region>.falcon.wtf and read the real rejection.

Retrying without a response

Keep the first signature of the transaction you sent. Poll it on a normal Solana RPC, and while it hasn't landed and the blockhash is still valid, resend the same signed bytes. Stop when it lands, or when the blockhash expires.

Don't re-sign to retry. A new signature is a distinct transaction, and it can land on its own.

Falcon does keep a signature-keyed cache of recent submissions and suppresses duplicates, but only once that signature has already been delivered, which is exactly why a landing-retry of the same bytes is still the right move. Delivery isn't exactly-once in either direction.

On this page