Developers
TypeScript SDK
@moxie/sdk builds Moxie instructions, sizes orders, replicates the matcher’s quote and decodes on-chain accounts. It has no runtime dependencies and works with any Solana client library.
Install#
The SDK lives in the Moxie monorepo at packages/sdk and isn’t published to npm yet. Inside the workspace, add it as a dependency:
"dependencies": {
"@moxie/sdk": "workspace:*",
"@solana/web3.js": "^1.98.0"
}| Entry point | Contents |
|---|---|
@moxie/sdk/trading | Order sizing, margin, matcher quotes and price limits. |
@moxie/sdk/instructions | Complete instructions (accounts + data) for portfolio, deposit, withdraw, trade and crank. |
@moxie/sdk/accounts | Decoders for imported-market records and portfolios. |
@moxie/sdk/percolator | Raw instruction-data encoders for the Percolator engine. |
@moxie/sdk/imported-market | Oracle encoders for listing, pricing and resolving markets (operator use; Node only). |
@moxie/sdk | Everything above. Pulls in node:crypto, so prefer the subpaths in browser code. |
Addresses you need#
Instructions take addresses as base58 strings. On devnet:
export const MOXIE = {
program: "9GnPynrGbmQqQw8715neTB6xqeEw7ZcpRTHCT3AEDM4e", // Percolator engine
matcherProgram: "H6z6Z5KSQtXj7sYvqX7oCwskBuhWvUDDB8KGNqeuNWXS",
market: "AihBkr17D9hSSyWRLK5cetZC2P5xMVbft112bHjZWgc2", // market group
usdcMint: "5yV4TPjnwvAGTmqJdjeM7rFZUVcrxSfPVVK87YNK3NRS", // test USDC
// Deployment-specific: collateral vault, LP portfolio, matcher context
// and matcher delegate. Take them from the deployment's .env
// (MOXIE_COLLATERAL_VAULT, MOXIE_LP_PORTFOLIO, MOXIE_MATCHER_CONTEXT,
// MOXIE_MATCHER_DELEGATE).
api: "http://localhost:8787", // indexer
};Converting instructions#
Builders return a library-neutral MoxieInstruction (programAddress, accounts, data). With @solana/web3.js:
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import type { MoxieInstruction } from "@moxie/sdk/instructions";
export const toWeb3 = (ix: MoxieInstruction) =>
new TransactionInstruction({
programId: new PublicKey(ix.programAddress),
keys: ix.accounts.map((a) => ({ pubkey: new PublicKey(a.address), isSigner: a.isSigner, isWritable: a.isWritable })),
data: Buffer.from(ix.data),
});Create a portfolio#
A portfolio is a 9,563-byte account owned by the engine. Allocate it with the System Program in the same transaction, then initialize it. The new account’s keypair must sign.
import { Keypair, PublicKey, SystemProgram, Transaction } from "@solana/web3.js";
import { createPortfolioInstruction } from "@moxie/sdk/instructions";
const PORTFOLIO_SIZE = 9563;
const portfolio = Keypair.generate();
const rent = await connection.getMinimumBalanceForRentExemption(PORTFOLIO_SIZE); // ~0.07 SOL
const tx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: owner.publicKey,
newAccountPubkey: portfolio.publicKey,
lamports: rent,
space: PORTFOLIO_SIZE,
programId: new PublicKey(MOXIE.program),
}),
toWeb3(createPortfolioInstruction({
program: MOXIE.program,
owner: owner.publicKey.toBase58(),
market: MOXIE.market,
portfolio: portfolio.publicKey.toBase58(),
})),
);
await sendAndConfirmTransaction(connection, tx, [owner, portfolio]);Deposit and withdraw#
Both need the portfolio’s current portfolioId and sequence, which you can read from the indexer (GET /v1/portfolios/:address) or decode yourself with decodePortfolioSummary.
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { depositInstruction } from "@moxie/sdk/instructions";
const p = await fetch(`${MOXIE.api}/v1/portfolios/${portfolioAddress}`).then((r) => r.json());
const ix = depositInstruction({
program: MOXIE.program,
owner: owner.publicKey.toBase58(),
market: MOXIE.market,
portfolio: portfolioAddress,
sourceToken: getAssociatedTokenAddressSync(new PublicKey(MOXIE.usdcMint), owner.publicKey).toBase58(),
vault: COLLATERAL_VAULT,
tokenProgram: TOKEN_PROGRAM_ID.toBase58(),
portfolioId: BigInt(p.portfolioId),
sequence: BigInt(p.sequence),
amount: 100_000_000n, // 100 USDC
});withdrawInstruction takes the same fields plus destinationToken and vaultAuthority, the PDA["vault", market] under the engine program:
const [vaultAuthority] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), new PublicKey(MOXIE.market).toBytes()],
new PublicKey(MOXIE.program),
);Place a trade#
A trade is four steps: size the order, quote it against the matcher, set a limit, and send.
import { planOrder, quoteMatcher, limitFor, FEE_BPS, MatcherCapacityError } from "@moxie/sdk/trading";
import { tradeInstruction } from "@moxie/sdk/instructions";
// 1. Market, trader and LP state from the indexer.
const market = await fetch(`${MOXIE.api}/v1/markets/${marketAddress}`).then((r) => r.json());
const trader = await fetch(`${MOXIE.api}/v1/portfolios/${portfolioAddress}`).then((r) => r.json());
const lp = await fetch(`${MOXIE.api}/v1/portfolios/${LP_PORTFOLIO}`).then((r) => r.json());
// 2. Size: spend up to $50 of free collateral going long at the current price.
const markE6 = BigInt(market.markE6);
const plan = planOrder("long", 50, Number(markE6) / 10_000);
if (!plan) throw new Error("Nothing to trade");
// 3. Quote exactly as the on-chain matcher will, then allow $0.02 of slippage.
const ctx = await connection.getAccountInfo(new PublicKey(MATCHER_CONTEXT));
let quote: bigint;
try {
quote = quoteMatcher(ctx!.data, markE6, plan.sizeQ);
} catch (e) {
if (e instanceof MatcherCapacityError) throw new Error("Too large for the LP right now; trade smaller.");
throw e;
}
// 4. Build and send.
const ix = tradeInstruction({
program: MOXIE.program,
traderAuthority: owner.publicKey.toBase58(),
market: MOXIE.market,
traderPortfolio: portfolioAddress,
lpPortfolio: LP_PORTFOLIO,
matcherProgram: MOXIE.matcherProgram,
matcherContext: MATCHER_CONTEXT,
matcherDelegate: MATCHER_DELEGATE,
traderPortfolioId: BigInt(trader.portfolioId),
traderPositionEpoch: BigInt(trader.positionEpoch),
lpPortfolioId: BigInt(lp.portfolioId),
lpPositionEpoch: BigInt(lp.positionEpoch),
lpMatcherSequence: BigInt(lp.sequence),
assetIndex: market.assetIndex,
marketId: BigInt(market.marketId),
sizeQ: plan.sizeQ,
feeBps: FEE_BPS,
limitPriceE6: limitFor(quote, plan.sizeQ),
});
const tx = new Transaction().add(
ComputeBudgetProgram.requestHeapFrame({ bytes: 128 * 1024 }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
toWeb3(ix),
);
await sendAndConfirmTransaction(connection, tx, [owner]);Closing a position
Closing is a trade of the opposite size. Positions decoded with decodePortfolioSummary carry a signed sizeQ, so pass -position.sizeQ as the trade size and quote it the same way.
Order sizing and margin#
| Function | What it does |
|---|---|
planOrder(side, amountUsd, priceCents, leverage?) | Largest order whose margin + fee fits in amountUsd. Returns contracts, signed sizeQ, notional, margin, fee and maxPayoutUsd, or null. |
marginPerContract(side, price, leverage?) | Initial margin per contract at price (0–1): long = price + 0.05; short = max(1 − price + 0.05, price). |
quoteMatcher(ctx, oracleE6, sizeQ) | The matcher’s fill price in E6 for this size, from the 320-byte matcher context. Throws MatcherCapacityError over the fill or inventory limit. |
limitFor(quoteE6, sizeQ, slippageE6?) | Limit price: the quote moved $0.02 (SLIPPAGE_E6) against the trader, clamped to (0, 1). |
FEE_BPS, BINARY_RISK_BUFFER, SLIPPAGE_E6 | 30 bps, 0.05, 20,000 E6. |
Decoding accounts#
import { decodePortfolioSummary, decodeImportedMarket, marketLifecycle } from "@moxie/sdk/accounts";
const info = await connection.getAccountInfo(new PublicKey(portfolioAddress));
const p = decodePortfolioSummary(info!.data);
console.log(Number(p.capital) / 1e6, "USDC");
for (const pos of p.positions) console.log(pos.assetIndex, pos.marketId, pos.sizeQ);
console.log("healthy:", p.health.valid);decodePortfolioSummary: owner, capital, PnL, funding paid/received per side, portfolioId, positionEpoch, sequence, up to 16 positions and the cached health certificate.decodeImportedMarket: one 384-byte market record — status, assetIndex, marketId, close time, mark, index, external and local bid/ask, basis TWAP and funding values.marketLifecycle(status):"active" | "restricted" | "reduce-only" | "locked" | "resolved".discoverActivePerps/discoverTradablePerps: filter decoded markets to those listed, or those accepting new risk.
Running a keeper#
Cranks are permissionless. Anyone can keep a market current with crankInstruction, passing the current slot and the asset indexes to update. The authority doesn’t need to sign; any portfolio in the group can be used as the crank portfolio.
import { crankInstruction } from "@moxie/sdk/instructions";
const slot = BigInt(await connection.getSlot());
const ix = crankInstruction({
program: MOXIE.program,
authority: payer.publicKey.toBase58(),
market: MOXIE.market,
portfolio: anyPortfolioAddress,
nowSlot: slot,
assets: [1, 2],
});Low-level encoders#
@moxie/sdk/percolator exposes the raw instruction data if you assemble accounts yourself. Layouts are covered by byte-exact tests in packages/sdk/test/wire-layout.test.ts.
| Encoder | Tag | Bytes |
|---|---|---|
buildCreatePortfolioData() | 1 | 1 |
buildDepositData(id, seq, amount) | 3 | 33 |
buildWithdrawData(id, seq, amount) | 4 | 33 |
buildPermissionlessCrankData(slot, assets) | 5 | 10 + 3 per asset |
buildTradeCpiData(request) | 10 | 85 |
buildCloseResolvedData(feeRatePerSlot?) | 30 | 17 |
buildHardFlatData(…) | 71 | 59 |
Operators listing markets use @moxie/sdk/imported-market: encodeActivateImportedPerp, encodePricingObservation, encodeResolutionObservation and hashIdentity.