选择传输方式
| JSON-RPC | POST /send | POST /send-bin | POST /send-batch | QUIC 单向 | QUIC 双向 | |
|---|---|---|---|---|---|---|
| 请求体 | JSON 中的 base64 | JSON 中的 base64 | 原始字节 | 最多 16 个原始帧 | 每个流一个数据包 | 每个流一个数据包 |
| 响应 | 签名或 JSON-RPC 错误 | 签名或 JSON 错误 | 签名或 JSON 错误 | 每帧的结果 | 无 | 准入码 |
| 最适合 | 直接替换现有代码 | 简单的 JSON 客户端 | HTTP 开销最低 | 一次请求发送多笔 | 最快的路径 | 直接获得拒绝原因 |
构建已签名并带小费的交易
本页的示例在每种语言中共用一个辅助模块。它构建一笔带有计算预算和小费的 memo 交易,对其签名,并且可以确认签名。直接运行它会以 base64 形式输出一笔已签名交易,cURL 示例使用的就是这个输出。 首先设置环境变量:export APEX_API_KEY="YOUR_API_KEY"
export APEX_RPC="http://fra.apex.orbitflare.com"
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
export KEYPAIR_PATH="payer.json"
// npm install @solana/web3.js
// Print a signed, tipped transaction as base64: node apex-tx.mjs
import {
ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
TransactionInstruction, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
export const APEX_RPC = process.env.APEX_RPC ?? "http://fra.apex.orbitflare.com";
export const API_KEY = process.env.APEX_API_KEY ?? "";
const SOLANA_RPC = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
const TIP_LAMPORTS = Number(process.env.TIP_LAMPORTS ?? 1_000_000);
const MEMO_PROGRAM = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
export const payer = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
);
export const connection = new Connection(SOLANA_RPC, "confirmed");
export async function apexRpc(method, params) {
const res = await fetch(APEX_RPC, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
});
const body = await res.json();
if (body.error) throw new Error(`${method} failed ${body.error.code}: ${body.error.message}`);
return body.result;
}
let tipAccounts;
let counter = 0;
// Returns a signed VersionedTransaction. Pass { tip: false } for a bundle member without a tip.
export async function buildTx({ tip = true, label = "apex" } = {}) {
tipAccounts ??= await apexRpc("getTipAccounts", []);
const tipAccount = new PublicKey(tipAccounts[Math.floor(Math.random() * tipAccounts.length)]);
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const instructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
new TransactionInstruction({
programId: MEMO_PROGRAM,
keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
data: Buffer.from(`${label} ${Date.now()} ${counter++}`),
}),
];
if (tip) {
instructions.push(
SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tipAccount, lamports: TIP_LAMPORTS }),
);
}
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
const tx = new VersionedTransaction(message);
tx.sign([payer]);
return tx;
}
// Poll getSignatureStatuses on the Solana RPC. Returns the slot, or null on timeout.
export async function confirm(signature, timeoutMs = 30_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { value } = await connection.getSignatureStatuses([signature]);
const status = value[0];
if (status?.err) throw new Error(`failed on chain: ${JSON.stringify(status.err)}`);
if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
return status.slot;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return null;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const tx = await buildTx();
console.log(Buffer.from(tx.serialize()).toString("base64"));
}
# pip install solders requests
# Print a signed, tipped transaction as base64: python apex_tx.py
import base64
import itertools
import json
import os
import random
import time
import requests
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.hash import Hash
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
APEX_RPC = os.environ.get("APEX_RPC", "http://fra.apex.orbitflare.com")
API_KEY = os.environ.get("APEX_API_KEY", "")
SOLANA_RPC = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
TIP_LAMPORTS = int(os.environ.get("TIP_LAMPORTS", "1000000"))
MEMO_PROGRAM = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
# One session keeps the HTTP connection to the Apex endpoint open between sends.
session = requests.Session()
session.headers.update({"x-api-key": API_KEY})
with open(os.environ.get("KEYPAIR_PATH", "payer.json")) as f:
payer = Keypair.from_bytes(bytes(json.load(f)))
_tip_accounts = None
_counter = itertools.count()
def apex_rpc(method, params):
r = session.post(APEX_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=5)
body = r.json()
if "error" in body:
raise RuntimeError(f"{method} failed {body['error']['code']}: {body['error']['message']}")
return body["result"]
def solana_rpc(method, params):
r = requests.post(SOLANA_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=10)
return r.json()["result"]
def build_tx(tip=True, label="apex"):
"""Returns a signed Transaction. Pass tip=False for a bundle member without a tip."""
global _tip_accounts
if _tip_accounts is None:
_tip_accounts = apex_rpc("getTipAccounts", [])
tip_account = Pubkey.from_string(random.choice(_tip_accounts))
blockhash = Hash.from_string(
solana_rpc("getLatestBlockhash", [{"commitment": "confirmed"}])["value"]["blockhash"])
instructions = [
set_compute_unit_limit(100_000),
set_compute_unit_price(10_000),
Instruction(MEMO_PROGRAM, f"{label} {time.time_ns()} {next(_counter)}".encode(),
[AccountMeta(payer.pubkey(), is_signer=True, is_writable=True)]),
]
if tip:
instructions.append(
transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=tip_account, lamports=TIP_LAMPORTS)))
return Transaction([payer], Message.new_with_blockhash(instructions, payer.pubkey(), blockhash), blockhash)
def confirm(signature, timeout_s=30.0):
"""Poll getSignatureStatuses on the Solana RPC. Returns the slot, or None on timeout."""
deadline = time.time() + timeout_s
while time.time() < deadline:
status = solana_rpc("getSignatureStatuses", [[signature]])["value"][0]
if status and status.get("err"):
raise RuntimeError(f"failed on chain: {status['err']}")
if status and status.get("confirmationStatus") in ("confirmed", "finalized"):
return status["slot"]
time.sleep(0.5)
return None
if __name__ == "__main__":
print(base64.b64encode(bytes(build_tx())).decode())
TX_BASE64=$(node apex-tx.mjs) # or: TX_BASE64=$(python apex_tx.py)
JSON-RPC sendTransaction
向 Apex 端点的根路径发送POST。这是标准的 Solana sendTransaction 格式,因此现有代码只需替换 URL 并添加 API 密钥即可迁移。
| 参数 | 类型 | 描述 |
|---|---|---|
params[0] | string | 已签名的交易,base64 编码 |
params[1].encoding | string | "base64" |
params[1].maxRetries | number,可选 | 该交易的重试预算。省略则使用端点的默认值 |
params[2] | boolean,可选 | mevProtect。true 会跳过 Shield 黑名单上的领导者。参见 MEV 保护 |
curl -s "$APEX_RPC" \
-H "Content-Type: application/json" \
-H "x-api-key: $APEX_API_KEY" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendTransaction\",\"params\":[\"$TX_BASE64\",{\"encoding\":\"base64\",\"maxRetries\":30},false]}"
import { apexRpc, buildTx, confirm } from "./apex-tx.mjs";
const tx = await buildTx({ label: "apex json-rpc" });
const signature = await apexRpc("sendTransaction", [
Buffer.from(tx.serialize()).toString("base64"),
{ encoding: "base64", maxRetries: 30 },
false, // mevProtect
]);
console.log("accepted:", signature);
console.log("landed in slot", await confirm(signature));
import base64
from apex_tx import apex_rpc, build_tx, confirm
tx = build_tx(label="apex json-rpc")
signature = apex_rpc("sendTransaction", [
base64.b64encode(bytes(tx)).decode(),
{"encoding": "base64", "maxRetries": 30},
False, # mevProtect
])
print("accepted:", signature)
print("landed in slot", confirm(signature))
// `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
use apex_sender_client::rpc::RpcClient;
use apex_sender_client::{serialize_transaction, Region};
let rpc = RpcClient::new(Region::Frankfurt, &api_key);
let wire = serialize_transaction(&tx)?;
// mev_protect = false, max_retries = Some(30)
let signature = rpc.send_transaction(&wire, false, Some(30)).await?;
println!("accepted: {signature}");
{ "jsonrpc": "2.0", "id": 1, "result": "<signature>" }
skipPreflight 和 preflightCommitment 不起作用。如果您需要模拟,请在发送前先在您的常规 RPC 上调用 simulateTransaction。
普通 HTTP 路由
这些路由与 JSON-RPC 位于同一主机和端口上,并省去了 JSON-RPC 封装。| 路由 | 请求体 | 成功响应 |
|---|---|---|
POST /send | JSON:{"transaction": "<base64>", "mevProtect"?: bool, "maxRetries"?: n} | {"signature": "..."} |
POST /send-bin | 原始交易字节,Content-Type: application/octet-stream | {"signature": "..."} |
POST /send-batch | 最多 16 帧,每帧为一个大端序 u16 长度后跟交易字节 | attempted、accepted、rejected 和 results |
POST /send-bundle | 1 到 4 帧,帧格式相同。参见捆绑包 | {"bundle_id": "...", "signatures": [...]} |
GET /ping | 无,且无需 API 密钥 | pong |
?mev_protect=1 和 &max_retries=N。
错误为带有 HTTP 状态码的 JSON:{"error": "<label>", "message": "..."}。参见错误与速率限制。
POST /send
curl -s "$APEX_RPC/send" \
-H "Content-Type: application/json" \
-H "x-api-key: $APEX_API_KEY" \
-d "{\"transaction\":\"$TX_BASE64\",\"mevProtect\":false,\"maxRetries\":30}"
import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";
const tx = await buildTx({ label: "apex send" });
const res = await fetch(`${APEX_RPC}/send`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
body: JSON.stringify({ transaction: Buffer.from(tx.serialize()).toString("base64"), mevProtect: false }),
});
const body = await res.json();
if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
console.log("accepted:", body.signature);
console.log("landed in slot", await confirm(body.signature));
import base64
from apex_tx import APEX_RPC, build_tx, confirm, session
tx = build_tx(label="apex send")
r = session.post(f"{APEX_RPC}/send",
json={"transaction": base64.b64encode(bytes(tx)).decode(), "mevProtect": False}, timeout=5)
body = r.json()
if "signature" not in body:
raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
print("accepted:", body["signature"])
print("landed in slot", confirm(body["signature"]))
POST /send-bin
开销最低的 HTTP 路径:传入时无需 base64,也无需 JSON。请求体就是序列化后的交易,逐字节一致。echo "$TX_BASE64" | base64 --decode | curl -s "$APEX_RPC/send-bin?mev_protect=0" \
-H "Content-Type: application/octet-stream" \
-H "x-api-key: $APEX_API_KEY" \
--data-binary @-
import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";
const tx = await buildTx({ label: "apex send-bin" });
const res = await fetch(`${APEX_RPC}/send-bin`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
body: tx.serialize(),
});
const body = await res.json();
if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
console.log("accepted:", body.signature);
console.log("landed in slot", await confirm(body.signature));
from apex_tx import APEX_RPC, build_tx, confirm, session
tx = build_tx(label="apex send-bin")
r = session.post(f"{APEX_RPC}/send-bin", headers={"Content-Type": "application/octet-stream"},
data=bytes(tx), timeout=5)
body = r.json()
if "signature" not in body:
raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
print("accepted:", body["signature"])
print("landed in slot", confirm(body["signature"]))
// `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
use apex_sender_client::rpc::RpcClient;
use apex_sender_client::{serialize_transaction, Region};
let rpc = RpcClient::new(Region::Frankfurt, &api_key);
let wire = serialize_transaction(&tx)?;
let signature = rpc.send_transaction_binary(&wire, false, None).await?;
println!("accepted: {signature}");
POST /send-batch
在一次请求中发送最多 16 笔相互独立的交易。请求体是一个帧序列:u16 big-endian length of the transaction bytes
bytes the serialized transaction
... repeated, up to 16 frames
: > batch.bin
for i in 1 2 3; do
node apex-tx.mjs | base64 --decode > tx.bin
printf '%04x' $(($(wc -c < tx.bin))) | xxd -r -p >> batch.bin # u16 big-endian length
cat tx.bin >> batch.bin
done
curl -s "$APEX_RPC/send-batch" \
-H "Content-Type: application/octet-stream" \
-H "x-api-key: $APEX_API_KEY" \
--data-binary @batch.bin
import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";
const txs = [];
for (let i = 0; i < 3; i++) txs.push(await buildTx({ label: `apex batch ${i}` }));
// Each frame: u16 big-endian length, then the transaction bytes.
const frames = txs.map((tx) => {
const wire = Buffer.from(tx.serialize());
const length = Buffer.alloc(2);
length.writeUInt16BE(wire.length);
return Buffer.concat([length, wire]);
});
const res = await fetch(`${APEX_RPC}/send-batch`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
body: Buffer.concat(frames),
});
const body = await res.json();
if (!body.results) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
console.log(`accepted ${body.accepted} of ${body.attempted}`);
for (const result of body.results) {
if (result.signature) console.log(result.signature, "landed in slot", await confirm(result.signature));
else console.log("rejected:", result.error, result.message);
}
import struct
from apex_tx import APEX_RPC, build_tx, confirm, session
txs = [bytes(build_tx(label=f"apex batch {i}")) for i in range(3)]
# Each frame: u16 big-endian length, then the transaction bytes.
payload = b"".join(struct.pack(">H", len(wire)) + wire for wire in txs)
r = session.post(f"{APEX_RPC}/send-batch", headers={"Content-Type": "application/octet-stream"},
data=payload, timeout=5)
body = r.json()
if "results" not in body:
raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
print(f"accepted {body['accepted']} of {body['attempted']}")
for result in body["results"]:
if "signature" in result:
print(result["signature"], "landed in slot", confirm(result["signature"]))
else:
print("rejected:", result["error"], result["message"])
// `txs` is a Vec of signed, tipped VersionedTransaction values.
use apex_sender_client::rpc::{BatchItem, RpcClient};
use apex_sender_client::{serialize_transaction, Region};
let rpc = RpcClient::new(Region::Frankfurt, &api_key);
let wires: Vec<Vec<u8>> = txs.iter().map(serialize_transaction).collect::<Result<_, _>>()?;
let refs: Vec<&[u8]> = wires.iter().map(Vec::as_slice).collect();
let batch = rpc.send_batch(&refs, false, None).await?;
println!("accepted {} of {}", batch.accepted, batch.attempted);
for item in &batch.results {
match item {
BatchItem::Accepted(signature) => println!("accepted: {signature}"),
BatchItem::Rejected { error, message } => println!("rejected: {error}: {message}"),
}
}
{
"attempted": 3,
"accepted": 2,
"rejected": 1,
"results": [
{ "signature": "<signature 1>" },
{ "signature": "<signature 2>" },
{ "error": "below_floor", "message": "..." }
]
}
GET /ping
返回pong。无需 API 密钥。用它在需要之前打开并预热 HTTP 连接,以及让空闲连接保持存活。参见最佳实践。
curl -s "$APEX_RPC/ping"
CORS
每个 HTTP 响应都带有Access-Control-Allow-Origin: *,并且 OPTIONS 预检请求会得到应答,因此上述所有路由都可以在浏览器代码中使用。在将密钥下发到前端之前,请参见浏览器注意事项。
QUIC
QUIC 是最快的接入方式。您与 Apex 端点保持一个持久连接,通过由您的 API 密钥派生的客户端证书完成一次身份验证,并为每笔交易打开一个流。在预热的连接上,一次发送就是打开一个流并写入一次,只占用客户端几微秒的时间。 流分为两种:| 流 | 返回内容 | 用途 |
|---|---|---|
| 单向 | 无。发出后不再理会 | 在最快路径上进行生产环境发送 |
| 双向 | 一个准入响应:已接受,或拒绝代码和消息 | 集成、调试,以及希望直接获得原因的工具 |
Rust
apex-sender-client crate 实现了整个传输层:证书、保活、0-RTT 恢复和重连。该程序在两种流上各发送一笔交易。它使用与快速入门相同的 Cargo.toml。
use std::str::FromStr;
use std::time::Duration;
use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Error, Region, MIN_TIP_LAMPORTS};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::{read_keypair_file, Keypair};
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;
async fn tipped_memo(
payer: &Keypair,
tip_accounts: &[Pubkey],
solana: &SolanaRpc,
label: &str,
) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let tip_account = tip::pick_tip_account(tip_accounts).ok_or("no tip accounts")?;
let instructions = [
ComputeBudgetInstruction::set_compute_unit_limit(100_000),
ComputeBudgetInstruction::set_compute_unit_price(10_000),
Instruction::new_with_bytes(
Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
format!("{label} {nonce}").as_bytes(),
vec![AccountMeta::new(payer.pubkey(), true)],
),
tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
];
let blockhash = solana.latest_blockhash().await?;
Ok(VersionedTransaction::from(Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
blockhash,
)))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("APEX_API_KEY")?;
let solana = SolanaRpc::new(
std::env::var("SOLANA_RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()),
);
let payer = read_keypair_file(std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into()))?;
let region = Region::Frankfurt;
let client = ApexSenderClient::connect(region, &api_key).await?;
let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;
// Unidirectional stream: fire and forget. Returns the signature, reads nothing back.
let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic uni").await?;
let signature = client.send_transaction(&tx).await?;
println!("sent: {signature}");
println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
// Bidirectional stream: the endpoint answers accepted or rejected before racing it.
let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic bidi").await?;
match client.send_transaction_with_response(&tx).await {
Ok(signature) => {
println!("accepted: {signature}");
println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
}
// Fix the transaction, not the client. The message names the floor when the tip is short.
Err(Error::Rejected { code, message }) => println!("rejected: {code:?}: {message}"),
Err(e) => return Err(e.into()),
}
Ok(())
}
准入码
双向流会返回以下之一:| 代码 | 名称 | 含义 |
|---|---|---|
0 | ok | 已接受。交易已被跟踪,并正在竞速送达领导者 |
1 | unauthorized | 该连接的密钥无效 |
2 | rate limited | 您的密钥超出了每秒限制 |
3 | invalid | 交易未通过合法性检查。消息会说明哪项未通过 |
4 | no tip | 未找到向 Apex 小费账户的转账 |
5 | below floor | 小费低于您所在等级的下限。消息中会注明下限 |
6 | busy | 端点正在卸载负载。请稍后重试 |
7 | malformed packet | 数据包的帧格式错误 |
在其他语言中使用 QUIC
您可以使用任何支持客户端证书的 QUIC 库,在任意语言中实现该传输层。 连接- QUIC (RFC 9000),使用 TLS 1.3 和 ALPN
solana-tpu。 - 服务器证书是自签名的占位证书。不要验证它。
- 客户端必须出示由您的 API 密钥派生的证书。
- 每个 Apex 端点保持一个打开的连接。定期发送 QUIC PING(Rust 客户端每秒 ping 一次)。端点的空闲超时为 30 秒。已启用 0-RTT。
u64 LE length of the transaction bytes
bytes the serialized transaction: legacy or v0 at most 1232 bytes, v1 at most 4096
u8 mev_protect: 1 to skip Shield-blocklisted leaders, else 0
u8 max_retries present: 0 or 1
u16 LE max_retries, only if present
{ wire_transaction: Vec<u8>, mev_protect: bool, max_retry: Option<u16> } 使用 bincode 默认的定长整数小端序选项进行编码的结果。整个数据包最大不得超过 4160 字节。
准入帧(仅限双向流)
u8 status: one of the admission codes above
[64] status 0 only: the transaction's first signature
u16 LE status != 0: message length
bytes status != 0: UTF-8 message
wire::encode_packet 和 wire::decode_admission 是这两种帧的参考实现。