> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rekt.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> A bot in one file that quotes, opens and closes a position through the SDK. Copy it, set two variables and run it.

This page gives you a bot to copy. It reads the chain, quotes a trade, opens it, waits for the fill,
closes it and prints what it was paid. It runs as a dry run until you tell it to send.

## Try it with no money

[Wheelman](https://docs.rekt.money/tools/wheelman.md) is a bot and command line built on this SDK. Its paper mode reads the
live markets, simulates the fill and sends nothing. A paper trade needs an RPC endpoint and no SOL
or USDC. Wheelman needs Node 22.18 or later, and it runs from a clone of its repository:

```sh theme={"dark"}
git clone https://github.com/Asymmetra/rekt-wheelman wheelman && cd wheelman
npm install && npm link
wheelman setup                     # a wallet, your RPC endpoint, the risk limits; writes .env
wheelman open BTC long 1 --paper   # a paper position: simulated, nothing sent
wheelman positions --paper         # the paper account
```

A paper result does not say what live trading pays.

The dry run of the bot on this page is different. It builds the real transaction and simulates it
on mainnet, so the wallet must hold SOL and USDC before the dry run works. A simulation refuses a fee
payer with no SOL, and the bot stops when the wallet has no USDC account.

## Set up

You need Node 20.19 or later (or Node 22.12 or later), a Solana keypair file, a little SOL for fees, and USDC in the wallet.

**1. Make a project.** The SDK is ESM-only, so the project needs `"type": "module"`.

```sh theme={"dark"}
mkdir rekt-bot && cd rekt-bot
npm init -y
npm pkg set type=module
npm install @rekt-trading/1000x@^0.6.0 @solana/web3.js
npm install --save-dev tsx typescript
```

**2. Make a keypair.** Save this as `keypair.ts` and run `npx tsx keypair.ts`. It writes the secret
key to `keypair.json` and prints only the public address. Keep that file out of version control and
out of every prompt and log.

```ts keypair.ts theme={"dark"}
import { writeFileSync } from 'node:fs';
import { Keypair } from '@solana/web3.js';

const key = Keypair.generate();
writeFileSync('keypair.json', JSON.stringify([...key.secretKey]), { mode: 0o600 });
console.log(key.publicKey.toBase58()); // the address to fund
```

**3. Fund the address.** Send about 0.01 SOL, which covers fees and the rent of the book a first trade
creates, and USDC on Solana for the stake. Buy USDC at an
exchange and withdraw it to that address on the Solana network, or swap SOL for USDC in a wallet and
send it over. The first USDC deposit creates the token account the bot uses. The smallest stake is
\$0.20. See [Fees and limits](https://docs.rekt.money/limits.md).

## The bot

Save this as `bot.ts`.

```ts bot.ts theme={"dark"}
// bot.ts: quote, open and close one position on Rekt Knockout.
//
//   npx tsx bot.ts           dry run: reads the chain, quotes, simulates, sends nothing
//   SEND=1 npx tsx bot.ts    sends: opens, waits for the fill, closes, prints the payout
//
// Set SOLANA_RPC_URL (an RPC endpoint) and KEYPAIR_PATH (a Solana keypair file). With SEND=1 this
// spends real USDC, so start with a small STAKE_USDC.
import { readFileSync } from 'node:fs';
import {
  ComputeBudgetProgram,
  Connection,
  Keypair,
  PublicKey,
  TransactionMessage,
  VersionedTransaction,
} from '@solana/web3.js';
import {
  assetId,
  chainNowSeconds,
  closableFromSlotOf,
  computeUnitLimit,
  configPda,
  connectionReader,
  decodeConfig,
  decodeMarket,
  decodeTraderAccount,
  decodeVault,
  errorForCode,
  findBySeq,
  ix,
  landedOpenedSeq,
  marketPda,
  openPreview,
  ourInstruction,
  parseUsdc,
  priceAccountsFor,
  programErrorCode,
  readSettlementForSeq,
  requestCloseFor,
  sizeForTotal,
  toHex,
  traderPda,
  vaultPda,
  type BuiltInstruction,
} from '@rekt-trading/1000x';

const SYMBOL = process.env['MARKET'] ?? 'BTC';
const BUDGET = parseUsdc(process.env['STAKE_USDC'] ?? '1.00'); // the most that leaves your wallet
const RUNG_BPS = 20_000; // a 2.00x target. Basis points of stake: 20_000 is the lowest target.
const SEND = process.env['SEND'] === '1';

const url = process.env['SOLANA_RPC_URL'];
const keypairPath = process.env['KEYPAIR_PATH'];
if (!url || !keypairPath) throw new Error('set SOLANA_RPC_URL and KEYPAIR_PATH');
const connection = new Connection(url, 'confirmed');
const wallet = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(readFileSync(keypairPath, 'utf8'))));

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const usd = (n: bigint) => `$${n / 1_000_000n}.${(n % 1_000_000n).toString().padStart(6, '0').slice(0, 2)}`;
/** A price from the chain is an integer at an exponent: 6_543_210_000_000n at -8 is 65432.10000000. */
const price = (raw: bigint, expo: number) => {
  if (expo >= 0) return (raw * 10n ** BigInt(expo)).toString();
  const digits = raw.toString().padStart(1 - expo, '0');
  return `${digits.slice(0, expo)}.${digits.slice(expo)}`;
};

async function read(address: PublicKey): Promise<Uint8Array | null> {
  const info = await connection.getAccountInfo(address, 'confirmed');
  return info === null ? null : new Uint8Array(info.data);
}

async function must(address: PublicKey, what: string): Promise<Uint8Array> {
  const bytes = await read(address);
  if (bytes === null) throw new Error(`${what} does not exist at ${address.toBase58()}`);
  return bytes;
}

/** Ask `probe` until it answers, for at most `ms`. */
async function until<T>(what: string, ms: number, probe: () => Promise<T | null>): Promise<T> {
  const deadline = Date.now() + ms;
  for (;;) {
    const answer = await probe();
    if (answer !== null) return answer;
    if (Date.now() > deadline) throw new Error(`timed out waiting for ${what}`);
    await sleep(500);
  }
}

/** Build, simulate and (with SEND=1) send one transaction. A refusal is reported by name. */
async function run(label: string, built: BuiltInstruction[], units: number): Promise<string | null> {
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
  const message = new TransactionMessage({
    payerKey: wallet.publicKey,
    recentBlockhash: blockhash,
    instructions: [
      ComputeBudgetProgram.setComputeUnitLimit({ units }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1_000 }),
      ...built.map(ourInstruction),
    ],
  }).compileToV0Message();
  const tx = new VersionedTransaction(message);
  tx.sign([wallet]);

  const sim = await connection.simulateTransaction(tx, { commitment: 'confirmed' });
  if (sim.value.err !== null) {
    const refusal = errorForCode(programErrorCode(sim.value.err) ?? -1);
    // `retryable` says what to do next: `never` means fix the request, `after-state-changes` means wait.
    throw new Error(
      `${label} refused: ${refusal ? `${refusal.name} (${refusal.retryable}) ${refusal.fix}` : JSON.stringify(sim.value.err)}`,
    );
  }
  if (!SEND) {
    console.log(`${label}: simulated, ${sim.value.unitsConsumed} compute units. Dry run: nothing sent.`);
    return null;
  }
  const signature = await connection.sendTransaction(tx);
  const done = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
  if (done.value.err !== null) throw new Error(`${label} reverted: ${signature}`);
  console.log(`${label}: landed ${signature}`);
  return signature;
}

async function main() {
  // 1. Read the protocol. Never cache it: every parameter is governed.
  const market = decodeMarket(await must(marketPda(assetId(SYMBOL)).address, `the ${SYMBOL} market`));
  const config = decodeConfig(await must(configPda().address, 'the config'));
  const vault = decodeVault(await must(vaultPda().address, 'the vault'));
  const nowUnix = BigInt(await chainNowSeconds(connection));
  const vaultToken = new PublicKey(Buffer.from(vault.tokenAccount, 'hex'));

  // 2. Quote. `sizeForTotal` turns a budget into a stake. `openPreview` says what it costs and pays.
  const size = sizeForTotal(BUDGET, config.params);
  if (size.refusal) throw new Error(`${size.refusal.error}: ${size.refusal.detail}`);
  const quote = openPreview(config, market, size.collateral, RUNG_BPS, nowUnix);
  if (quote.barrierPayout === null) throw new Error('the engine refuses these terms');
  console.log(
    `quote ${SYMBOL}: stake ${usd(size.collateral)}, you pay ${usd(quote.totalDebit)}, ` +
      `gross at the target ${usd(quote.maxPayout)} (before the exit fee and the share of the gain the terms withhold), ` +
      `at the barrier ${usd(quote.barrierPayout)}`,
  );

  // 3. Open. Your USDC account is your wallet's associated token account on the vault's mint.
  const mint = new PublicKey((await must(vaultToken, 'the vault token account')).subarray(0, 32));
  const [usdc] = PublicKey.findProgramAddressSync(
    [wallet.publicKey.toBytes(), new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA').toBytes(), mint.toBytes()],
    new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'),
  );
  await must(usdc, 'your USDC account');
  const book = traderPda(wallet.publicKey).address; // one account holds all your positions
  const me = toHex(wallet.publicKey); // the program takes addresses as hex
  const accounts = {
    config: toHex(configPda().address),
    market: toHex(marketPda(assetId(SYMBOL)).address),
    vault: toHex(vaultPda().address),
    vault_token: toHex(vaultToken),
    positions: toHex(book),
  };
  const open = ix.open(
    {
      authority: me,
      trader_token: toHex(usdc),
      config: accounts.config,
      market: accounts.market,
      vault: accounts.vault,
      vault_token: accounts.vault_token,
      protocol_token: config.treasury,
      positions: accounts.positions,
      delegate: null,
    },
    {
      budget: BUDGET,
      direction: 0, // 0 long, 1 short
      rung_bps: RUNG_BPS,
      user_timer_s: 0, // no timer of your own
      min_collateral_out: size.collateral, // the smallest stake you accept for this budget
      referrer: null,
      tier: 0, // 0 is the market's own leverage
      opener_tag: 0n,
    },
    // The market's own list: the governance table on a feed-kind-3 market, then the fee-terms account
    // when the market has one. No quote on either kind.
    priceAccountsFor(market, config, 'open'),
  );
  // A first trade also creates the book, in the same transaction.
  const first = (await read(book)) === null;
  const createBook = first
    ? [
        ix.initTraderAccount(
          {
            trader: me,
            payer: me,
            positions: accounts.positions,
            vault: accounts.vault,
            vault_token: accounts.vault_token,
            payout_destination: toHex(usdc), // every payout of this book goes here
          },
          4, // room for four positions at once; 1 to 12
        ),
      ]
    : [];
  const units =
    computeUnitLimit('open') +
    (first ? computeUnitLimit('init_trader_account') : 0);
  const signature = await run('open', [...createBook, open], units);
  if (signature === null) return; // a dry run stops here

  // 4. The open is an order. The venue's next price fills it, or returns it with your whole stake.
  const landed = await connection.getTransaction(signature, { commitment: 'confirmed', maxSupportedTransactionVersion: 0 });
  const opened = landedOpenedSeq({
    logs: landed?.meta?.logMessages ?? null,
    returnData: null,
    owner: me,
    market: accounts.market,
  });
  if (opened.seq === null) throw new Error(opened.why);
  const seq = opened.seq; // the handle of your position; never use the slot index
  const find = async () => {
    const bytes = await read(book);
    return bytes === null ? null : findBySeq(decodeTraderAccount(bytes), seq);
  };
  const filled = await until('the fill', 60_000, async () => {
    const position = await find();
    if (position === null) return 'returned' as const; // the order was returned
    return position.entryPrice !== 0n ? position : null;
  });
  if (filled === 'returned') return console.log('the order was returned: your whole stake came back');
  console.log(`filled at ${price(filled.entryPrice, filled.entryExpo)}`); // entryPrice alone is not a price

  // 5. Close. A close is a request, and the venue's keepers complete it. You cannot ask in the first
  //    slots after the entry; the SDK says which slot is the first.
  const from = closableFromSlotOf(filled, config, market);
  while (from !== null && BigInt(await connection.getSlot('confirmed')) < from) await sleep(400);
  await run(
    'close',
    [
      // `requestCloseFor` adds the governance table on a feed-kind-3 market, which refuses a request without it.
      requestCloseFor(
        market,
        { authority: me, config: accounts.config, market: accounts.market, positions: accounts.positions, delegate: null },
        seq,
      ),
    ],
    computeUnitLimit('request_close'),
  );
  await until('the settlement', 120_000, async () => ((await find()) === null ? true : null));

  // 6. Read what the close paid. Part of a win can be paid later: see "how a win is paid".
  const [trade] = await readSettlementForSeq(connectionReader(connection), wallet.publicKey, [seq], config);
  console.log(trade ? `closed (${trade.reason}): paid ${usd(trade.payouts.trader)} now` : 'closed: read the settlement from your history');
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  process.exit(1);
});
```

## Run it

```sh theme={"dark"}
export SOLANA_RPC_URL="<your RPC endpoint>"
export KEYPAIR_PATH="$PWD/keypair.json"

npx tsx bot.ts            # a dry run: quotes and simulates, sends nothing
SEND=1 npx tsx bot.ts     # sends: opens, waits for the fill, closes
```

Keep the RPC endpoint in an environment variable and out of your source and your logs.
Endpoints from a provider carry a key in the URL.

## What each step does

1. **Read the protocol.** The config, the vault and the market are decoded from the chain. Nothing
   here is cached: every parameter is governed and can move.
2. **Quote.** `sizeForTotal` turns the budget you hand the venue into a stake. `openPreview` says
   what it costs, the gross payout at the target (before the exit fee and the share of the gain the
   terms withhold) and what you receive at the barrier, which is nothing. A close between the entry
   and the barrier pays what is left after the loss, less a loss tax on that loss and then the exit
   fee, so a loss costs more than the move and the exit fee. See
   [what a close pays](https://docs.rekt.money/sdk/positions.md#what-every-close-would-pay-right-now). Show the barrier line next to the button in anything a person
   uses.
3. **Open.** The open is an order. The stake is taken when it lands, and the venue's next price after
   a short delay fills it. A first trade creates your book in the same transaction. `min_collateral_out`
   is your floor on the stake, so a fee change in between reverts the open and never shrinks it.
4. **Wait for the fill.** The order is filled, or it is returned and your whole stake comes back. A
   returned order is a normal outcome. The bot reads your book until it knows which. The handle of a
   position is its `seq`. A slot index is not a handle, because slots are reused.
5. **Close.** A close is a request that carries no price. You cannot ask in the first slots after the
   entry, and `closableFromSlotOf` says which slot is the first. `requestCloseFor` builds the request
   with the accounts the market's feed kind needs. The venue's keepers complete the close. Anyone may.
6. **Read the payout.** Part of a win can be paid later, in order. The bot prints what was paid now.
   See [how a win is paid](https://docs.rekt.money/liquidity.md#how-a-win-is-paid).

## When something is refused

The bot simulates before it sends, so a refusal costs nothing. It prints the name of the refusal and
its retry class. Branch on the class and not on the name or the text:

| Class | Do |
| - | - |
| `never` | Fix the request. A retry is a loop. |
| `after-state-changes` | Wait for the world to move, then ask again. |
| `after-governance` | Stop. Only a change by the venue's authority clears it. |
| `immediately` | Retry on the next slot. |

[Error handling](https://docs.rekt.money/sdk/errors.md) lists every refusal a client sees. [Reading gates](https://docs.rekt.money/sdk/gates.md) predicts
them before you send.

## Next

> [card] [Orders and closes](https://docs.rekt.money/sdk/orders.md)
>
> The contract a bot builds against: fills, returns, close requests and the events for each.

> [card] [Read the protocol](https://docs.rekt.money/sdk/reading.md)
>
> The read path on its own, with the oracle shape and the gate report.

> [card] [Fees and limits](https://docs.rekt.money/limits.md)
>
> What a trade costs and the limits it meets. Dated, and they can change.

> [card] [Build with an AI agent](https://docs.rekt.money/sdk/ai.md)
>
> The docs as an MCP server, `llms.txt`, and a skill file an agent can load.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.