Builder API reference
This page lists every endpoint of the DoubleZero builder API on testnet, and explains how to sign the instructions it returns. For a walkthrough that uses these endpoints, see Publish a feed on testnet.
Basics
| Base URL | https://subscription-api-testnet.doublezero.xyz |
| Authentication | None |
| Rate limit | 10 requests per 10 seconds from each IP address |
| Request and response format | JSON. Send content-type: application/json with a body. |
{builder} in a path |
Your builder key, the public key of your keypair |
| 2Z amounts | Base units. 1 2Z is 100000000. |
| Rates and bandwidth | Bits per second |
Read endpoints return public chain data, so they answer for any builder key. Write endpoints never change state. Each one returns an unsigned instruction that you sign and send yourself. The API never holds your key.
Errors
A refused request returns an HTTP error status and a JSON body that names the problem:
{"error": "bond 9Qx... holds 0 and its rate requires 100000000"}
| Status | Meaning |
|---|---|
404 |
The path or the account it names does not exist. No body. |
422 |
A field is missing or malformed, or the request breaks a ledger rule, such as a code already in use. |
429 |
Rate limited. Wait for the next 10-second window. The body is not JSON. |
502 |
The API could not read Solana or the DoubleZero ledger. Retry. |
503 |
The request is valid but must wait, such as a bond the ledger has not mirrored yet. Retry in a minute. |
The API applies the ledger's rules before it builds an instruction, so most refusals arrive here rather than from the chain. Some checks only the chain can make, so a signed transaction can still fail.
Signing
Every write endpoint returns the same four fields, plus the addresses of the accounts it creates or changes:
{
"program_id": "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb",
"accounts": [
{"pubkey": "HTt...", "is_signer": true, "is_writable": true},
{"pubkey": "7Gm...", "is_signer": false, "is_writable": true}
],
"data": "AQIDBA==",
"signers": ["HTt..."]
}
| Field | Meaning |
|---|---|
program_id |
The program the instruction calls |
accounts |
The instruction's accounts, in order, with whether each signs and whether it is written |
data |
The instruction data, base64 |
signers |
Who must sign. Always your builder key alone. |
To complete a write:
- Check that
signersholds your builder key and nothing else. - Build an instruction from
program_id,accounts, and the decodeddata. - Put it in a transaction with your key as the fee payer and a recent blockhash from the right chain.
- Sign with your key and send.
The API returns an instruction rather than a transaction because a blockhash expires in about a minute. You fetch the blockhash when you are ready to sign, and you can add instructions of your own, such as a priority fee.
| Endpoints | Send to |
|---|---|
/bonds and below |
Solana devnet, https://api.devnet.solana.com |
| Everything else | DoubleZero testnet ledger, https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16 |
Signing script
This script follows the steps above with @solana/web3.js. Install Node.js 20 or later, then:
mkdir -p ~/dz-sign && cd ~/dz-sign
npm init -y && npm install @solana/web3.js@1
Save the script as ~/dz-sign/sign.mjs:
// Signs an instruction from the DoubleZero builder API and sends it.
// Usage: curl ... | node sign.mjs <rpc-url> <keypair-file>
import { readFileSync } from "node:fs";
import {
Connection,
Keypair,
PublicKey,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
const [rpcUrl, keypairFile] = process.argv.slice(2);
const body = readFileSync(0, "utf8");
let answer = {};
try {
answer = JSON.parse(body);
} catch {}
if (!answer.program_id) {
console.error(`The API refused the request: ${body}`);
process.exit(1);
}
const signer = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(keypairFile, "utf8"))),
);
const builder = signer.publicKey.toBase58();
if (answer.signers.length !== 1 || answer.signers[0] !== builder) {
console.error(`The API built this for ${answer.signers}, but the keypair is ${builder}`);
process.exit(1);
}
const instruction = new TransactionInstruction({
programId: new PublicKey(answer.program_id),
keys: answer.accounts.map((account) => ({
pubkey: new PublicKey(account.pubkey),
isSigner: account.is_signer,
isWritable: account.is_writable,
})),
data: Buffer.from(answer.data, "base64"),
});
const connection = new Connection(rpcUrl, "confirmed");
try {
const signature = await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[signer],
);
console.log(signature);
} catch (err) {
console.error(err.message);
process.exit(1);
}
Pipe a write endpoint's response into it:
curl -sS -X POST $API/builders/$BUILDER/feeds/$FEED_KEY/halt \
| node ~/dz-sign/sign.mjs $LEDGER_RPC ~/.config/doublezero/id.json
It prints the transaction signature. If the API refused the request, it prints the API's error and sends nothing. Any Solana SDK works the same way if you prefer another language.
Reads
GET /builders/requirements
What a bond must hold at each tier.
{
"tiers": [
{"name": "up-to-1gbps", "max_rate_bits_per_sec": 1000000000, "required_2z_amount": 100000000},
{"name": "up-to-5gbps", "max_rate_bits_per_sec": 5000000000, "required_2z_amount": 200000000},
{"name": "unmetered", "max_rate_bits_per_sec": null, "required_2z_amount": 500000000}
]
}
max_rate_bits_per_sec is null for the unmetered tier. A required_2z_amount of 0 means the tier is not priced yet, and a bond at that tier cannot be posted.
GET /builders/{builder}/bonds
Every bond the builder has created, with what each holds and needs.
[
{
"bond_key": "9Qx...",
"bond_index": 0,
"bonded_2z_amount": 100000000,
"required_2z_amount": 100000000,
"committed_rate_bits_per_sec": 1000000000,
"hold_expires_at": 1808578800,
"funded": true
}
]
| Field | Meaning |
|---|---|
bond_index |
The index you chose when you created the bond. Other endpoints take it to name the bond. |
hold_expires_at |
When the hold ends, in Unix seconds. null before the first post. |
funded |
Whether the bond holds what its committed rate requires. A bond that is not funded backs no feed. |
GET /builders/{builder}/feeds
The builder's feeds and the multicast groups it created on its bonds, as the DoubleZero ledger holds them.
{
"feeds": [
{
"feed_key": "965...",
"code": "acme-tob",
"name": "Acme top of book",
"exchange": "4kF...",
"exchange_code": "xfra",
"exchange_name": "Frankfurt",
"status": "active",
"chain": "unspecified",
"groups": ["EAm..."],
"builder": "HTt...",
"bond_ref": "9Qx..."
}
],
"multicast_groups": [
{
"multicast_group": "EAm...",
"code": "acme-tob-fra",
"multicast_ip": "233.84.178.10",
"status": "activated",
"publishers": 1,
"subscribers": 0,
"owner": "HTt...",
"bond_ref": "9Qx..."
}
]
}
A feed's status is active, halted, retiring, or retired. groups holds multicast group addresses.
Bonds
Bond instructions go to Solana devnet. Each response is a signing response plus bond_key, the bond's address.
POST /builders/{builder}/bonds
Creates a bond. The bond holds no 2Z until you post to it. You pay the rent for the bond and its token account.
| Field | Type | Meaning |
|---|---|---|
bond_index |
integer | Any index you have not used. Indexes need not be consecutive. |
committed_rate_bits_per_sec |
integer | The rate the bond commits to. It sets the tier. |
{"bond_index": 0, "committed_rate_bits_per_sec": 1000000000}
Refused when the rate is 0, its tier is not priced, or you already have a bond at that index.
POST /builders/{builder}/bonds/{bond_index}/post
Moves 2Z into a bond. The first post starts the bond's hold.
| Field | Type | Meaning |
|---|---|---|
amount |
integer | 2Z, in base units |
token_account |
string, optional | The token account to take the 2Z from. Defaults to your associated 2Z account. |
{"amount": 100000000}
POST /builders/{builder}/bonds/{bond_index}/withdraw
Moves 2Z out of a bond. Takes the same fields as post, with token_account as the destination. After the hold ends, you can withdraw what the bond holds above its requirement. A request in the last seconds of the hold can pass the API and still be refused by the chain.
Multicast groups
Group instructions go to the DoubleZero ledger.
POST /builders/{builder}/multicast-groups
Creates a multicast group on one of your bonds. You own the group.
| Field | Type | Meaning |
|---|---|---|
code |
string | The group code, unique across the network |
max_bandwidth_bits_per_sec |
integer | The group's bandwidth. The bond's tier must cover it. |
bond_index |
integer | The bond the group is created on |
{"code": "acme-tob-fra", "max_bandwidth_bits_per_sec": 1000000000, "bond_index": 0}
The response adds multicast_group_key and bond_key. The group's address depends on how many accounts the ledger holds, so send the transaction promptly. If the ledger refuses it because another account was created first, request it again.
Returns 503 until the protocol has copied a newly funded bond to the ledger, which takes about a minute.
POST /builders/{builder}/multicast-groups/{multicast_group}/publishers
Lets a host publish to your group. If the publisher has no access pass, this creates one that you own. You also pay the publisher's connect credits.
| Field | Type | Meaning |
|---|---|---|
client_ip |
string | The publishing host's public IPv4 address |
user_payer |
string | The key that pays for the publisher's access pass. Usually your builder key. |
{"client_ip": "203.0.113.10", "user_payer": "HTt..."}
The response adds multicast_group_key, access_pass_key, and creates_access_pass, which is true when this request creates the pass.
DELETE /builders/{builder}/multicast-groups/{multicast_group}/publishers
Removes a publisher from your group. Takes client_ip and user_payer as query parameters:
curl -sS -X DELETE "$API/builders/$BUILDER/multicast-groups/$GROUP_KEY/publishers?client_ip=$HOST_IP&user_payer=$BUILDER"
Feeds
Feed instructions go to the DoubleZero ledger.
POST /builders/{builder}/feeds
Creates a feed backed by one of your bonds. The feed is active once the transaction lands. A bond backs one feed.
| Field | Type | Meaning |
|---|---|---|
code |
string | The feed code |
name |
string | The feed's display name |
exchange |
string | The metro's address, from doublezero exchange list |
groups |
array of strings | Multicast group addresses |
spec_id |
string | The wire spec, as <spec>@<version>. See Choose a spec. |
committed_rate_bits_per_sec |
integer | The feed's committed rate. The bond's tier must cover it. |
bond_index |
integer | The bond that backs the feed |
{
"code": "acme-tob",
"name": "Acme top of book",
"exchange": "4kF...",
"groups": ["EAm..."],
"spec_id": "top-of-book@v3",
"committed_rate_bits_per_sec": 1000000000,
"bond_index": 0
}
The response adds feed_key and bond_key.
POST /builders/{builder}/feeds/{feed_key}/halt
Stops a feed from admitting new subscribers. Existing subscribers stay connected. Only an active feed can be halted. No body.
POST /builders/{builder}/feeds/{feed_key}/resume
Returns a halted feed to active. A feed that the DoubleZero Foundation halted can only be resumed by the DoubleZero Foundation. No body.
POST /builders/{builder}/feeds/{feed_key}/retire
Starts the feed's 30-day retirement notice. The feed keeps publishing during the notice and is retired when it ends. Retiring is permanent. No body.
Each feed response adds feed_key.