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

# Quickstart

> Open a wager from TypeScript on devnet, hand it to a keeper, and read the result.

This guide opens a wager on devnet from a Node script with [`@solana/kit`](https://github.com/anza-xyz/kit) and a client generated from Fade's IDL. The wallet plays every role: app, payer, stake owner and beneficiary.

<Info>
  Requirements: Node 20 or later, a devnet keypair with about **0.02 devnet SOL** and some **test USDC**. Claim both from the faucet in [app.fade.finance](https://app.fade.finance), or call the faucet API (step 2).
</Info>

<Steps>
  <Step title="Install and generate the client">
    ```bash theme={"dark"}
    npm init -y && npm pkg set type=module
    npm install --save-exact @solana/kit@8.3.0 @solana/program-client-core@8.3.0 @solana-program/token@0.16.1
    npm install --save-exact --save-dev codama@1.11.0 @codama/nodes-from-anchor@1.5.6 @codama/renderers-js@2.5.0 tsx
    curl -o fade.json https://fade.finance/idl/fade.json
    ```

    The IDL is the Anchor IDL of the devnet deployment, served as a text file: its program address is the devnet program and its USDC mint the devnet test mint. Generate the client:

    ```js generate.mjs theme={"dark"}
    import { readFileSync } from "node:fs";
    import { createFromRoot } from "codama";
    import { rootNodeFromAnchor } from "@codama/nodes-from-anchor";
    import { renderVisitor } from "@codama/renderers-js";

    const idl = JSON.parse(readFileSync("fade.json", "utf8"));
    createFromRoot(rootNodeFromAnchor(idl)).accept(
      renderVisitor(".", {
        generatedFolder: "src/generated/fade",
        deleteFolderBeforeRendering: true,
        formatCode: false,
        syncPackageJson: false,
      }),
    );
    ```

    ```bash theme={"dark"}
    node generate.mjs
    ```

    You now have `src/generated/fade` with a builder for every instruction (`getOpenWagerInstructionAsync`, …), account fetchers (`fetchConfig`, `fetchMaybeWager`, …), PDA helpers, error codes and event decoders.
  </Step>

  <Step title="Get test USDC">
    The faucet sends 1 000 test USDC, and a little SOL when the wallet has almost none. One claim per wallet and per IP every 5 minutes.

    ```bash theme={"dark"}
    curl -X POST https://api.fade.finance/v1/faucet \
      -H 'content-type: application/json' \
      -d '{"owner":"<your wallet address>"}'
    # {"signature":"…","usdc":"1000000000","sol":"0"}
    ```

    If the wallet still lacks SOL, use [faucet.solana.com](https://faucet.solana.com).
  </Step>

  <Step title="Set up the connection">
    ```ts src/fade.ts theme={"dark"}
    import {
      address,
      appendTransactionMessageInstructions,
      assertIsTransactionWithBlockhashLifetime,
      createSolanaRpc,
      createSolanaRpcSubscriptions,
      createTransactionMessage,
      getSignatureFromTransaction,
      pipe,
      sendAndConfirmTransactionFactory,
      setTransactionMessageFeePayerSigner,
      setTransactionMessageLifetimeUsingBlockhash,
      signTransactionMessageWithSigners,
      type Instruction,
      type TransactionSigner,
    } from "@solana/kit";

    export const PROGRAM = address("2jZZEa56EtYm2hdstDrYZPYw1phAUny25k9GKAWKNFq4");
    export const USDC_MINT = address("E4XYMYALrvMX19P6TxqKXe8ZYGxfj2CjMub6c2ZficTv"); // test USDC, 6 decimals
    export const KEEPER = "https://api.fade.finance";
    export const cfg = { programAddress: PROGRAM };

    export const rpc = createSolanaRpc("https://api.devnet.solana.com");
    const rpcSubscriptions = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
    const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

    export async function send(feePayer: TransactionSigner, instructions: Instruction[]) {
      const { value: blockhash } = await rpc.getLatestBlockhash().send();
      const message = pipe(
        createTransactionMessage({ version: 0 }),
        (m) => setTransactionMessageFeePayerSigner(feePayer, m),
        (m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
        (m) => appendTransactionMessageInstructions(instructions, m),
      );
      const tx = await signTransactionMessageWithSigners(message);
      assertIsTransactionWithBlockhashLifetime(tx);
      await sendAndConfirm(tx, { commitment: "confirmed" });
      return getSignatureFromTransaction(tx);
    }
    ```

    The generated builders default to a program address taken from the IDL. Pass `cfg` anyway, so the code says which deployment it targets.
  </Step>

  <Step title="Build a paytable">
    Probabilities are integers in billionths and must sum to exactly 1 000 000 000. Multipliers are integers in basis points: 10 000 is 1×. Compute in `bigint`, never in floating point.

    ```ts src/paytable.ts theme={"dark"}
    import type { BucketArgs } from "./generated/fade";

    const P_SCALE = 1_000_000_000n;
    const BPS = 10_000n;

    /** Pays `target` or nothing for `stake`, keeping `edgeBps` for the house. */
    export function allOrNothing(stake: bigint, target: bigint, edgeBps: bigint): BucketArgs[] {
      const m = (target * BPS + stake - 1n) / stake; // multiplier, rounded up
      const p = ((BPS - edgeBps) * P_SCALE) / m; //       probability, rounded down
      return [
        { p, m },
        { p: P_SCALE - p, m: 0n },
      ];
    }
    ```

    `allOrNothing(1_000_000n, 10_000_000n, 350n)` returns `[{ p: 96_500_000n, m: 100_000n }, { p: 903_500_000n, m: 0n }]`: 10× at 9.65 %, a 3.5 % edge. More shapes and the sizing rules are on [Paytables](/build/paytables).
  </Step>

  <Step title="Open the wager">
    ```ts src/open.ts theme={"dark"}
    import { getProgramDerivedAddress, type TransactionSigner } from "@solana/kit";
    import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
    import {
      fetchConfig,
      fetchMaybeIntegrator,
      findConfigPda,
      findIntegratorPda,
      findWagerPda,
      getOpenWagerInstructionAsync,
    } from "./generated/fade";
    import { PROGRAM, USDC_MINT, cfg, rpc, send } from "./fade";
    import { allOrNothing } from "./paytable";

    export async function openWager(user: TransactionSigner) {
      const [configPda] = await findConfigPda(cfg);
      const { protocolTreasury } = (await fetchConfig(rpc, configPda)).data;

      const [usdcAccount] = await findAssociatedTokenPda({
        owner: user.address,
        mint: USDC_MINT,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
      });

      // The app record is created by its first wager: until then the nonce
      // is 0 and the fee account is the one passed now.
      const [integrator] = await findIntegratorPda({ integratorAuthority: user.address }, cfg);
      const record = await fetchMaybeIntegrator(rpc, integrator);
      const nonce = record.exists ? record.data.nextNonce : 0n;
      const feeAccount = record.exists ? record.data.feeAccount : usdcAccount;

      const [eventAuthority] = await getProgramDerivedAddress({
        programAddress: PROGRAM,
        seeds: ["__event_authority"],
      });

      const stake = 1_000_000n; // 1 USDC
      const open = await getOpenWagerInstructionAsync(
        {
          integratorAuthority: user, // the app's identity
          payer: user, //               SOL side; refunds come back here
          stakeOwner: user, //          owns the staked USDC
          stakeSource: usdcAccount,
          feeAccount,
          protocolTreasury,
          beneficiary: usdcAccount, //  the only account a payout can go to
          usdcMint: USDC_MINT,
          eventAuthority,
          program: PROGRAM,
          stake,
          paytable: allOrNothing(stake, 10_000_000n, 350n),
          integratorFeeBps: 0n,
          nonce,
        },
        cfg,
      );

      const signature = await send(user, [open]);
      const [wager] = await findWagerPda({ integrator, nonce }, cfg);
      return { wager, signature };
    }
    ```

    The builder derives `config`, `pool`, `integrator`, `vaultUsdc`, `wager`, `vrfPayer` and ORAO's network state for you. If a check fails, sending throws with the program's error; see [Errors](/build/errors).
  </Step>

  <Step title="Hand it to a keeper">
    The randomness request and the settlement are permissionless. The public keeper sends both:

    ```ts src/keeper.ts theme={"dark"}
    import type { Address } from "@solana/kit";
    import { KEEPER } from "./fade";

    export type KeeperWager = {
      state: "queued" | "requested" | "fulfilled" | "settled" | "expired" | "unknown";
      openSlot?: number;
      requestSignature?: string;
      settleSignature?: string;
      expireSignature?: string;
      bucket?: number; // index of the paytable row drawn
      payout?: string; // USDC base units
      updatedAt: string;
    };

    export async function settleWithKeeper(wager: Address): Promise<KeeperWager> {
      await fetch(`${KEEPER}/v1/wagers`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ wager }),
      });
      for (;;) {
        const view = (await (await fetch(`${KEEPER}/v1/wagers/${wager}`)).json()) as KeeperWager;
        if (view.state === "settled" || view.state === "expired") return view;
        await new Promise((r) => setTimeout(r, 1_500));
      }
    }
    ```

    Even without the `POST`, the keeper sweeps for open wagers about every 30 seconds; posting just starts it at once.
  </Step>

  <Step title="Run it">
    ```ts src/main.ts theme={"dark"}
    import { readFileSync } from "node:fs";
    import { createKeyPairSignerFromBytes } from "@solana/kit";
    import { openWager } from "./open";
    import { settleWithKeeper } from "./keeper";

    const secret = new Uint8Array(JSON.parse(readFileSync(process.argv[2]!, "utf8")));
    const user = await createKeyPairSignerFromBytes(secret);

    const { wager, signature } = await openWager(user);
    console.log("opened", wager, `https://explorer.solana.com/tx/${signature}?cluster=devnet`);

    const result = await settleWithKeeper(wager);
    console.log(result.state, "bucket", result.bucket, "payout", result.payout);
    ```

    ```bash theme={"dark"}
    npx tsx src/main.ts ~/devnet-wallet.json
    # opened 7Vb…  https://explorer.solana.com/tx/…?cluster=devnet
    # settled bucket 1 payout 0
    ```

    `bucket` is the index of the paytable row that was drawn: `0` is the 10× row here, `1` is nothing. A payout is in USDC base units and has already reached the beneficiary.
  </Step>
</Steps>

## Settling without a keeper

Anyone may send the two remaining steps, including your own server or the user's wallet. Request the randomness from two slots after the open, wait for ORAO to fulfil, then settle.

```ts src/settle-yourself.ts theme={"dark"}
import {
  getAddressDecoder,
  getAddressEncoder,
  getBase64Encoder,
  getProgramDerivedAddress,
  getU64Encoder,
  type Address,
  type TransactionSigner,
} from "@solana/kit";
import {
  fetchMaybeWager,
  getRequestRandomnessInstructionAsync,
  getSettleWagerInstructionAsync,
} from "./generated/fade";
import { PROGRAM, USDC_MINT, cfg, rpc, send } from "./fade";

const ORAO = "VRFzZoJdhFWL8rkvu87LpKM3RbcVezpMEc6X5GVDr7y" as Address;
const SLOT_HASHES = "SysvarS1otHashes111111111111111111111111111" as Address;

async function accountBytes(a: Address) {
  const { value } = await rpc.getAccountInfo(a, { encoding: "base64", commitment: "confirmed" }).send();
  return value ? new Uint8Array(getBase64Encoder().encode(value.data[0])) : null;
}

/** The first SlotHashes entry strictly after `slot` (entries are newest first). */
async function firstSlotHashAfter(slot: bigint) {
  const data = await accountBytes(SLOT_HASHES);
  if (!data) return null;
  const view = new DataView(data.buffer, data.byteOffset);
  let found: { slot: bigint; hash: Uint8Array } | null = null;
  for (let i = 0; i < Number(view.getBigUint64(0, true)); i++) {
    const offset = 8 + i * 40; // u64 slot, then a 32-byte hash
    const s = view.getBigUint64(offset, true);
    if (s <= slot) break;
    found = { slot: s, hash: data.slice(offset + 8, offset + 40) };
  }
  return found;
}

async function sha256(...parts: Uint8Array[]) {
  const buf = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
  let o = 0;
  for (const p of parts) (buf.set(p, o), (o += p.length));
  return new Uint8Array(await crypto.subtle.digest("SHA-256", buf));
}

async function eventAuthority() {
  return (await getProgramDerivedAddress({ programAddress: PROGRAM, seeds: ["__event_authority"] }))[0];
}

export async function requestRandomness(caller: TransactionSigner, wager: Address) {
  const w = await fetchMaybeWager(rpc, wager, { commitment: "confirmed" });
  if (!w.exists) throw new Error("wager not found");
  const { openSlot } = w.data;

  // Allowed from open slot + 2, once the next slot's hash is in SlotHashes.
  let entropy = null;
  while (!entropy) {
    const slot = await rpc.getSlot({ commitment: "confirmed" }).send();
    if (slot >= openSlot + 2n) entropy = await firstSlotHashAfter(openSlot);
    if (!entropy) await new Promise((r) => setTimeout(r, 400));
  }

  // The program derives the same seed and refuses any other request account.
  const u64 = (v: bigint) => new Uint8Array(getU64Encoder().encode(v));
  const addr = (a: Address) => new Uint8Array(getAddressEncoder().encode(a));
  const seed = await sha256(
    new TextEncoder().encode("FADE/ORAO-SEED/v1"),
    addr(PROGRAM),
    addr(wager),
    u64(openSlot),
    u64(entropy.slot),
    entropy.hash,
  );
  const [request] = await getProgramDerivedAddress({
    programAddress: ORAO,
    seeds: ["orao-vrf-randomness-request", seed],
  });

  // ORAO's treasury, read from its network state (bytes 40..72).
  const [networkState] = await getProgramDerivedAddress({
    programAddress: ORAO,
    seeds: ["orao-vrf-network-configuration"],
  });
  const state = await accountBytes(networkState);
  if (!state) throw new Error("ORAO network state not readable");
  const oraoTreasury = getAddressDecoder().decode(state.subarray(40, 72));

  const ix = await getRequestRandomnessInstructionAsync(
    { caller, wager, oraoTreasury, request, eventAuthority: await eventAuthority(), program: PROGRAM },
    cfg,
  );
  await send(caller, [ix]);
  return request;
}

/** ORAO's randomness account carries state tag 1 at byte 8 once fulfilled. */
export async function waitFulfilled(request: Address) {
  for (;;) {
    const data = await accountBytes(request);
    if (data && data[8] === 1 && data.length >= 137) return;
    await new Promise((r) => setTimeout(r, 1_000));
  }
}

export async function settle(caller: TransactionSigner, wager: Address) {
  const w = await fetchMaybeWager(rpc, wager, { commitment: "confirmed" });
  if (!w.exists) throw new Error("wager already closed");
  const ix = await getSettleWagerInstructionAsync(
    {
      caller,
      integrator: w.data.integrator,
      wager,
      randomness: w.data.randomness,
      beneficiary: w.data.beneficiary,
      payer: w.data.payer,
      usdcMint: USDC_MINT,
      eventAuthority: await eventAuthority(),
      program: PROGRAM,
    },
    cfg,
  );
  return send(caller, [ix]);
}
```

The caller of each step is paid its share of the crank fee (50 000 lamports to request, 200 000 to settle). If ORAO has not fulfilled within `T_SETTLE` (150 slots), `settle_wager` is impossible and anyone may call `expire_wager` instead. See [Settlement and keepers](/build/settlement).

## Reading the result

* **From the keeper:** `GET /v1/wagers/:address` returns `bucket`, `payout` and the settle signature.
* **From the chain:** the wager account closes at settlement. The settle transaction carries a `WagerSettled` event with `bucket`, `payout` and `pending` (true when the payout was parked because the beneficiary could not receive). See [Events](/build/events) for decoding.
* **While open:** `fetchMaybeWager` returns the stored paytable, `status` (`Open`, `Requested` or `PayoutPending`), `openSlot`, `expirySlot`, the bound `randomness` account and the reserved liability.

## Next

* Size prizes against the pool: [Paytables](/build/paytables).
* Who pays which SOL and gets it back: [Fees](/build/fees).
* Run your own keeper: [Settlement and keepers](/build/settlement).


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