ZeroCopyZeroCopy
API reference

Raw HTTP

Send raw transaction and bundle bytes over HTTP with /binary and /plaintext, and the payload envelope they share.

The raw routes take a transaction's wire bytes instead of a JSON-RPC request. They serve the same hosts, ports and authentication as JSON-RPC.

RouteKeyBodySuccess
POST /binaryyesA payload, raw bytes200, text/plain
POST /plaintextyesA payload, base64 text200, text/plain
GET /healthnononeHealth check
GET /tip_flooryesnoneTip floor

POST /binary

Send one payload: a signed transaction or an envelope with a declared tip or bundle. The maximum body is 16,403 bytes, received within 2 seconds.

PayloadSuccess body
One transactionIts first signature, base58
A bundleThe bundle id, 64 hex characters

There is no encoding option or preflight simulation. The same tip, rate and transaction rules apply as for JSON-RPC.

POST /plaintext

The body is the same payload in standard base64 with padding, on one line. Whitespace before and after it is ignored. The body may be up to 21,888 bytes, and the decoded payload up to 16,403 bytes. Responses are those of /binary.

Payload envelope

Every raw channel carries this payload: /binary, /plaintext, QUIC datagrams and streams, and the agent's IPC records.

A payload whose first byte is not 0x00 is one signed transaction in wire format.

A payload that starts with 0x00 is an envelope:

OffsetSizeField
010x00
11Flags: 0x01 declared tip present, 0x02 bundle body. No other bits may be set.
28Declared tip in lamports, u64 little endian. Present only when flags include 0x01.
2 or 10restThe body.

Without the 0x02 flag, the body is one transaction. With it, the body is:

SizeField
1Transaction count, 1 to 4
2Length of the next transaction, u16 little endian, 1 to 4,096
lengthThe transaction's wire bytes

The length and transaction pair repeats once per transaction, and no bytes may follow the last one. The largest payload, a declared-tip bundle of four 4,096-byte transactions, is 16,403 bytes.

PayloadBytes
One transactionTX
One transaction, declared tip of 1,000,000 lamports00 01 40 42 0f 00 00 00 00 00 TX
Bundle of two00 02 02 L1 L1 TX1 L2 L2 TX2
Bundle of two, declared tip of 1,000,000 lamports00 03 40 42 0f 00 00 00 00 00 02 L1 L1 TX1 L2 L2 TX2

Here L1 L1 is the little-endian length of TX1. An envelope that breaks these rules is refused as malformed with problem invalid_envelope, or as bundle_size when the count is 0 or above 4.

Responses

A rejection is a JSON object with the HTTP status below: the reason, its fields, message, retryable and docs, as in the JSON-RPC error.data.

Rejection (example from /binary)
{
  "reason": "missing_tip",
  "minimum": 1000000,
  "found": 123,
  "message": "tip too small: the tip transfers add up to 123 lamports, the minimum is 1,000,000; raise the transfer to one of the tip accounts to at least 1,000,000 lamports",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#missing_tip"
}

A GET to any path other than /health and /tip_floor, and any method other than GET and POST, is refused with 404 and problem not_found. A POST to any path other than the raw routes is JSON-RPC.

Examples

POST /binary
curl -sS "https://sol.zerocopy.sh/binary" \
  -H "x-api-key: $SWQOS_API_KEY" \
  -H 'content-type: application/octet-stream' \
  --data-binary @signed-tx.bin
POST /plaintext
base64 -w0 signed-tx.bin | curl -sS "https://sol.zerocopy.sh/plaintext" \
  -H "x-api-key: $SWQOS_API_KEY" \
  --data-binary @-

base64 -w0 is GNU coreutils; on macOS, base64 -i signed-tx.bin writes one line.

This helper builds envelopes. It needs no dependencies.

payload.ts
export function transactionPayload(
  transaction: Uint8Array,
  declaredTip?: bigint,
): Uint8Array<ArrayBuffer> {
  if (declaredTip === undefined) return concat([transaction])
  return concat([Uint8Array.of(0x00, 0x01), u64(declaredTip), transaction])
}

export function bundlePayload(
  transactions: Uint8Array[],
  declaredTip?: bigint,
): Uint8Array<ArrayBuffer> {
  if (transactions.length < 1 || transactions.length > 4) {
    throw new RangeError('a bundle holds 1 to 4 transactions')
  }
  const parts: Uint8Array[] = [Uint8Array.of(0x00, declaredTip === undefined ? 0x02 : 0x03)]
  if (declaredTip !== undefined) parts.push(u64(declaredTip))
  parts.push(Uint8Array.of(transactions.length))
  for (const transaction of transactions) parts.push(u16(transaction.length), transaction)
  return concat(parts)
}

function u64(value: bigint): Uint8Array<ArrayBuffer> {
  const bytes = new Uint8Array(8)
  new DataView(bytes.buffer).setBigUint64(0, value, true)
  return bytes
}

function u16(value: number): Uint8Array<ArrayBuffer> {
  const bytes = new Uint8Array(2)
  new DataView(bytes.buffer).setUint16(0, value, true)
  return bytes
}

function concat(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
  const out = new Uint8Array(parts.reduce((size, part) => size + part.length, 0))
  let offset = 0
  for (const part of parts) {
    out.set(part, offset)
    offset += part.length
  }
  return out
}
send-binary.ts
import { readFile } from 'node:fs/promises'
import { bundlePayload } from './payload.ts'
import { required, url } from './swqos.ts'

// Signed transaction files, in bundle order.
const transactions = await Promise.all(
  process.argv.slice(2).map(async (file) => new Uint8Array(await readFile(file))),
)
const response = await fetch(`${url}/binary`, {
  method: 'POST',
  headers: { 'x-api-key': required('SWQOS_API_KEY') },
  body: bundlePayload(transactions),
})
const body = await response.text()
console.log(response.ok ? `bundle ${body}` : `refused ${response.status}: ${body}`)

send-binary.ts uses the url and required exports of the client helper.

On this page