Send over QUIC
The low latency path. Open one QUIC connection to send.swqos.com:11000, hold it for the life of your process, and open a single bidirectional stream per submission. Identity is established once at handshake, so no send pays for authentication.
Why QUIC is faster here#
Over HTTPS, every submission pays for a connection setup unless your client is careful about pooling, and it re-presents your credential on every request. Over QUIC you connect once. The TLS handshake happens once, your identity is resolved once from the client certificate, and after that a submission is a stream write on a connection that is already open.
Measured relay overhead on this path is 6.29 ms at the median and 32.3 ms at the 99th percentile on production traffic.
The protocol#
1ALPN ultrasend/1 structured receipts and errors2 solana-tpu raw compatibility mode, no per-stream reply3Endpoint send.swqos.com:110004Stream one bidirectional stream per submission5Write the serialized transaction, then finish the send half6Read one JSON object from the receive half, capped at 4 KiB7Payload at most 1232 bytes8Keepalive hold the connection open; do not reconnect per send
Use ultrasend/1 whenever you want a structured receipt or a structured error. It is the branded protocol and it is what every first party client speaks.
Stream lifecycle#
- Open one bidirectional stream.
- Write the serialized transaction as raw bytes. No JSON wrapper, no base64.
- Finish the send half so we know the payload is complete.
- Read one JSON object from the receive half. Responses are capped at 4 KiB.
On success
1{2 "receipt": {3 "signature": "5Nx...",4 "accepted": true,5 "duplicate": false,6 "charged_lamports": 200000,7 "balance_remaining_lamports": 498000008 }9}
On failure
1{2 "error": {3 "code": "INSUFFICIENT_BALANCE",4 "message": "insufficient prepaid balance"5 }6}
The error codes are the same ones the HTTPS path returns, and they are all listed in errors. The send and stream timeout is 1000 ms.
A client in every language#
There is no bearer header on this path. The client derives an Ed25519 keypair from your API key and presents a self-signed certificate carrying that key, which is the same convention Solana’s own TPU clients use. Each program below connects once, then opens one stream per submission.
1import { Agent } from "undici";23// Node's QUIC support is still experimental, so the fastest reliable path from4// Node today is HTTP/1.1 with a warm keep-alive pool rather than raw QUIC. This5// removes the TCP and TLS handshake from every send, which is most of what the6// QUIC path buys you.7//8// If you want true QUIC from a JS runtime, run the Rust or Go client above as a9// sidecar and talk to it over a local socket.1011const agent = new Agent({12 keepAliveTimeout: 60_000,13 keepAliveMaxTimeout: 300_000,14 connections: 4, // a small warm pool, mirroring the QUIC guidance15 pipelining: 1,16});1718const API_KEY = process.env.SWQOS_API_KEY!;1920export async function send(tx: Uint8Array) {21 const res = await fetch("https://send.swqos.com/v1/transactions", {22 method: "POST",23 headers: {24 Authorization: `Bearer ${API_KEY}`,25 "Content-Type": "application/json",26 },27 body: JSON.stringify({28 transaction: Buffer.from(tx).toString("base64"),29 }),30 // @ts-expect-error undici dispatcher is not in the DOM fetch types31 dispatcher: agent,32 });3334 if (!res.ok) {35 const { error } = await res.json();36 throw new Error(`${error.code}: ${error.message}`);37 }38 return res.json();39}
Compatibility mode#
We also accept solana-tpu, which takes raw unidirectional transaction streams for clients built against the standard Solana TPU protocol. There is no per-stream response in this mode, so you get no receipt and no error detail. Failures surface as connection close codes instead.
| Close code | Meaning |
|---|---|
0x100 | Unauthorized |
0x101 | Account disabled |
0x102 | Insufficient balance |
Use compatibility mode only when you cannot change the client. If you can, ultrasend/1 gives you a receipt with the charge and your remaining balance, which is worth having.
The mistake that costs you everything#
Keepalive is 25 seconds and the maximum idle timeout is five minutes, so a connection survives quiet periods without any work on your side. If your process is long lived, so is the connection.
If your transactions are bursty and you want headroom, open a small pool of connections at startup and round robin across them. Even two is enough to keep one warm while another is in use.