> ## 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.

## Set up

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

<Warning>
  **The SDK is not on npm yet, so the install command below works from the first publish.** The
  pages in this section describe its interface.
</Warning>

**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/1000x @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 for fees, 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](/limits).

## 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,
  sizeForTotal,
  toHex,
  traderPda,
  vaultPda,
  type BuiltInstruction,
} from '@rekt/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)}`;

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,
    },
    priceAccountsFor(market, config, 'open'), // adds the market's fee-terms account when it has one
  );
  // 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', { quotePost: false }) +
    (first ? computeUnitLimit('init_trader_account', { quotePost: false }) : 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 ${filled.entryPrice}`);

  // 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',
    [
      ix.requestClose(
        { authority: me, config: accounts.config, market: accounts.market, positions: accounts.positions, delegate: null },
        seq,
      ),
    ],
    computeUnitLimit('request_close', { quotePost: false }),
  );
  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. 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. 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](/liquidity#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](/sdk/errors) lists every refusal a client sees. [Reading gates](/sdk/gates) predicts
them before you send.

## Next

<CardGroup cols={2}>
  <Card title="Orders and closes" icon="list-check" href="/sdk/orders">
    The contract a bot builds against: fills, returns, close requests and the events for each.
  </Card>

  <Card title="Read the protocol" icon="book-open-reader" href="/sdk/reading">
    The read path on its own, with the oracle shape and the gate report.
  </Card>

  <Card title="Fees and limits" icon="scale-balanced" href="/limits">
    What a trade costs and the limits it meets. Dated, and they can change.
  </Card>

  <Card title="Build with an AI agent" icon="robot" href="/sdk/ai">
    The docs as an MCP server, `llms.txt`, and a skill file an agent can load.
  </Card>
</CardGroup>


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