API reference
The SWQoS JSON-RPC envelope, authentication, method index, error format and status codes.
SWQoS provides JSON-RPC, raw HTTP and QUIC for sending transactions and bundles. Use your Solana RPC provider for blockhashes, balances and confirmation.
Endpoint
Send JSON-RPC requests with POST to any path of a SWQoS host other than /binary and /plaintext. https://sol.zerocopy.sh/, https://sol.zerocopy.sh/rpc and https://fra.sol.zerocopy.sh/ are equivalent. Endpoints lists the hosts and ports.
| Request | Served as |
|---|---|
POST /binary | Raw transaction or bundle bytes |
POST /plaintext | Raw payload as base64 text |
GET /health | Health check, no key |
GET /tip_floor | Tip floor |
POST to any other path | JSON-RPC |
| Any other method or path | 404 with reason invalid_request, problem not_found |
Every JSON-RPC method needs an API key: the api-key query parameter, an x-api-key header or Authorization: Bearer. See Authentication.
Request
A request body is one JSON object of at most 65,536 bytes, received within 2 seconds.
| Member | Rule |
|---|---|
jsonrpc | Exactly "2.0". |
method | One of the methods below. |
params | An array. Required by sendTransaction and sendBundle; the other methods ignore it. |
id | Optional. Echoed in the response exactly as sent; null when absent. |
Batches (a JSON array of requests) are refused with problem batch; send one request per call. HTTP/2 connections can carry many requests at once.
{"jsonrpc":"2.0","id":1,"method":"getTipAccounts"}Methods
| Method | Params | Result |
|---|---|---|
sendTransaction | [transaction, options?] | Base58 signature string |
sendBundle | [[transaction, ...], options?], 1 to 4 transactions | 64-character hex bundle id |
getTipAccounts | none | Array of base58 tip account addresses |
getBundleTipAccounts | none | The same array |
getTipFloor | none | Array of zero or one tip floor objects |
getHealth | none | "ok" |
getSlot | none | Integer slot |
Any other method name is refused with problem unknown_method and code -32601. There is no getLatestBlockhash, getSignatureStatuses, simulateTransaction, bundle status method or WebSocket subscription.
Responses
A result comes back with HTTP 200:
{"jsonrpc":"2.0","result":RESULT,"id":ID}A refusal carries the rejection in error.data:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "tip missing: add a System transfer of at least 1,000,000 lamports to one of the tip accounts (getTipAccounts or getBundleTipAccounts), or declare a tip",
"data": {
"reason": "missing_tip",
"minimum": 1000000,
"found": 0,
"message": "tip missing: add a System transfer of at least 1,000,000 lamports to one of the tip accounts (getTipAccounts or getBundleTipAccounts), or declare a tip",
"retryable": false,
"docs": "https://docs.zerocopy.sh/errors#missing_tip"
}
},
"id": 1
}| Field | Meaning |
|---|---|
data.reason | Stable snake_case reason. Match on this. |
Other data fields | The numbers behind the refusal, specific to each reason. See Errors. |
data.message, error.message | The same sentence: what happened and what to change. Wording can change. |
data.retryable | Whether sending the same request again, unchanged, can succeed. |
data.docs | https://docs.zerocopy.sh/errors# followed by the reason. |
A failed preflight simulation also puts Solana's simulation result fields, such as err, logs and unitsConsumed, at the top level of data. An unhealthy getHealth is the one error without data: {"code":-32005,"message":"Node is unhealthy"}.
id is null when the request was refused before its id could be read: a missing or unknown key, a body that is too large, unreadable or late, invalid JSON, a batch, or a body that is not a JSON object.
Status codes
JSON-RPC answers most refusals with HTTP 200, as Solana RPC does. It uses another status where a client acts on it.
| Reason | HTTP | JSON-RPC code |
|---|---|---|
unauthorized | 401 | -32000 |
rate_limited | 429 | -32001 |
simulation_failed | 200 | -32002 |
payment_required | 402 | -32003 |
not_ready, overloaded, forward_failed, preflight_unavailable, internal | 503 | -32603 |
malformed, too_large, unsigned, signature_count, missing_tip, credit_exhausted, bundle_size, bundle_control | 200 | -32602 |
invalid_request | 200 | by problem, below |
unsupported (SWQoS agent only) | 200 | -32601 |
invalid_request problem | JSON-RPC code |
|---|---|
parse_error | -32700 |
not_found, body_timeout, body_unreadable, batch, not_json_rpc | -32600 |
unknown_method | -32601 |
missing_params, invalid_params, transaction_not_string, invalid_options, invalid_commitment | -32602 |
Codes -32001 and -32003 have other meanings in Solana's RPC (a cleaned-up block and a failed signature verification). Identify a SWQoS refusal by error.data.reason, not by the numeric code.
Solana client libraries
sendTransaction has Solana's request shape, so Solana clients can call it. They do not surface SWQoS rejections in full. With @solana/kit 8.5:
- A response with HTTP status 401, 402, 429 or 503 throws
SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERRORcarrying onlystatusCodeand headers. Kit does not read the body, soreason,retry_after_msandunpaid_lamportsare lost. - A
simulation_failedrefusal throws Kit's preflight failure error, which keeps the simulation fields. - A refusal with code -32602, -32600, -32601 or -32700 throws an error that keeps only the message text, not
data.reasonor its fields.
Use Kit, or another Solana client, for building, signing and the happy path. Use a plain HTTP request, like the helper below, when your code needs to act on the reason.
Client helper
The JSON-RPC examples in this reference use this helper. It needs only fetch, and works in Bun and Node.js 18 or later. It reads the key from SWQOS_API_KEY and the endpoint from SWQOS_URL.
export type Rejection = {
reason: string
message: string
retryable: boolean
docs: string
[field: string]: unknown
}
type Reply<T> =
| { result: T }
| { error: { code: number; message: string; data?: Rejection } }
export class SwqosError extends Error {
readonly status: number
readonly code: number
readonly rejection: Rejection | undefined
constructor(status: number, code: number, message: string, rejection?: Rejection) {
super(rejection?.message ?? message)
this.status = status
this.code = code
this.rejection = rejection
}
}
export function required(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`set ${name}`)
return value
}
export const url = process.env.SWQOS_URL ?? 'https://sol.zerocopy.sh'
const apiKey = required('SWQOS_API_KEY')
export async function call<T>(method: string, params?: unknown[]): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
})
const reply = (await response.json()) as Reply<T>
if ('error' in reply) {
const { code, message, data } = reply.error
throw new SwqosError(response.status, code, message, data)
}
return reply.result
}Other interfaces
- Raw HTTP:
/binary,/plaintext,/health,/tip_floorand the payload envelope. - QUIC: ALPN
zerocopy-swqos/1, client-certificate keys, streams and reply frames. - Local agent: the same methods on your own host, with listed differences.