Skip to content

Quickstart

The fastest way to build on capped futures is the futures API. Reads need no API key and no wallet. A trade needs a wallet with USDC on Solana mainnet, and nothing else: the API’s sponsor pays the network fee. Trades spend real USDC.

These examples use plain HTTP; the SDK’s FuturesClient wraps the same routes in TypeScript. The examples use Node.js 22 or later, which has fetch built in, and the futures API at https://api.amoeba.farm. Any language with an HTTP client works the same way. To see the raw data first:

Terminal window
curl -s https://api.amoeba.farm/v1/markets

Save this as read-markets.mjs:

const API = process.env.AMOEBA_API ?? "https://api.amoeba.farm";
async function api(path, body) {
const response = await fetch(new URL(path, API), body === undefined ? {} : {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const json = await response.json();
if (!response.ok) throw new Error(`${response.status} ${json.error}`);
return json;
}
const { markets } = await api("/v1/markets");
for (const market of markets) {
const expiry = new Date(market.expiryTs * 1000).toISOString();
console.log(
`${market.label.padEnd(13)} ${market.status.padEnd(8)} index ${market.index?.value ?? "-"}` +
` last ${market.lastEntryValue ?? "-"} suggested ${market.suggestion.lowValue}-${market.suggestion.highValue}` +
` expires ${expiry}`,
);
}

Run it with node read-markets.mjs. Each line is one market: its status, the oracle index, the last price on the price line, the ticket’s suggested range and the expiry.

// Append to read-markets.mjs, or import api() from it.
const quote = await api("/v1/quote", { market: "RAMX", side: "long", lo: 90, hi: 110, lots: 100 });
if (quote.error) console.log(`Not fillable now: ${quote.error}`);
const usd = (atoms) => (Number(atoms) / 1e6).toFixed(2);
console.log(`Fills ${quote.filled} of ${quote.lots} lots at an average entry of ${quote.averageEntryValue}`);
console.log(`You pay ${usd(quote.pay)} USDC; it pays up to ${usd(quote.maxPayout)} USDC`);

pay is what a taker puts in, and maxPayout is what the filled lots pay at most. Amounts are strings of USDC atoms (6 decimals); the example converts them for display only. Keep the original strings for anything you compute or send back. GET /v1/markets/RAMX/depth?lo=90&hi=110 shows the same range level by level.

This script quotes a long, prepares it with a limit 2% worse than the quote, checks the prepared transaction, signs it and submits it. It reads the key file named by KEYPAIR and never prints it. It needs @solana/web3.js, and the wallet needs USDC (mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v). It spends real money: try it with a few lots, or stop before the submit to inspect the prepared transaction without sending it.

import fs from "node:fs";
import { Keypair, VersionedTransaction } from "@solana/web3.js";
// api() as in read-markets.mjs
const owner = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(process.env.KEYPAIR, "utf8"))));
const ownerKey = owner.publicKey.toBase58();
const config = await api("/v1/config");
const market = await api("/v1/markets/RAMX");
const [lo, hi] = [90, 110];
// Quote, then limit the order to the quote's average price per lot, 2% worse, within the range's value.
const quote = await api("/v1/quote", { market: market.market, side: "long", lo, hi, lots: 10 });
if (quote.error || quote.filled === 0) throw new Error(quote.error ?? "nothing fills");
const set = BigInt(market.k) * BigInt(hi - lo);
const average = BigInt(quote.premium) / BigInt(quote.filled);
const limit = [average + average / 50n + 1n, set].reduce((a, b) => (a < b ? a : b));
const prepared = await api("/v1/tx/prepare", {
owner: ownerKey,
action: "submit",
params: { market: market.market, side: "long", lo, hi, lots: quote.filled, limit: limit.toString() },
});
// Check before signing: the sponsor pays, only the owner and the sponsor sign, only expected programs run.
const tx = VersionedTransaction.deserialize(Buffer.from(prepared.transaction, "base64"));
const keys = tx.message.staticAccountKeys.map((key) => key.toBase58());
const signers = keys.slice(0, tx.message.header.numRequiredSignatures);
const allowed = new Set([
"ComputeBudget111111111111111111111111111111",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
config.programs.futures,
]);
if (keys[0] !== config.sponsor) throw new Error("not paid by the sponsor");
if (!signers.includes(ownerKey) || signers.some((key) => key !== ownerKey && key !== config.sponsor)) {
throw new Error("unexpected signers");
}
for (const instruction of tx.message.compiledInstructions) {
if (!allowed.has(keys[instruction.programIdIndex])) throw new Error("unexpected program");
}
tx.sign([owner]);
const { signature } = await api("/v1/tx/submit", {
id: prepared.id,
transaction: Buffer.from(tx.serialize()).toString("base64"),
});
const landed = await api(`/v1/tx/${signature}/wait?commitment=confirmed`);
console.log(signature, landed.status, landed.error ?? "");

The checks above are the minimum. Petri also checks that every futures instruction targets this market with the owner’s own accounts and does exactly what was asked; see Prepared transactions.

If the book changed before the transaction landed, it fails with an error such as NotBestPrice and changes nothing. Quote and prepare again.

Before you rely on the data, or sign anything built from it, compare the API’s identity with values you trust, and ask your RPC for its genesis hash too:

const TRUSTED = {
genesisHash: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d", // Solana mainnet
futures: "Bvp9kiRtJ7dT8unxP4Da39gv2eXDLEHLpcDfzcEuRKMu",
usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
};
const config = await api("/v1/config");
if (
config.genesisHash !== TRUSTED.genesisHash ||
config.programs.futures !== TRUSTED.futures ||
config.usdcMint !== TRUSTED.usdcMint
) {
throw new Error("Not Amoeba's mainnet deployment");
}

Identity and compatibility explains what else to check.