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

# First Trade

> Fund a test account, place a post-only order, inspect its result, and cancel the remainder.

This walkthrough uses direct transactions on the current Monad testnet MON/USDC market. It needs no Relay account. Use a dedicated owner with no existing orders in that market and no other process trading for it.

## Before you run

Install the [reviewed SDK release](/sdk/installation), obtain test MON for gas and dummy USDC, and check [Testnet Setup](/deployments/setup). This script sends real testnet transactions and deposits the amount you choose. Re-running it deposits that amount again.

Set these environment values through your local secret manager or shell environment:

| Variable | Meaning |
| - | - |
| PRIVATE\_KEY | Dedicated test owner's key |
| RPC\_URL | Monad testnet RPC |
| LIMIT\_PRICE | Maximum USDC per MON, as a decimal string |
| BASE\_QUANTITY | MON quantity, as a decimal string |
| DEPOSIT\_USDC | USDC to deposit, including sufficient fee headroom |

The current minimum notional is 10 USDC; the script reads bounds from the market. Choose a valid tick-aligned price and enough funds for the buy reservation and maker fee. A post-only order can be skipped if it crosses when executed, or can fill after resting before cancellation. Use test funds and inspect the outcome.

From a local docs checkout, run:

```bash theme={null}
node --experimental-strip-types examples/src/first-order.ts
```

## Complete example

```typescript theme={null}
import { abi, createKuruClient, decodeKuruEventLog, decodeBookUpdatesPacked, NativeExecInstruction, NativeSide, NativeTif } from "@toxicflow-labs/ts-sdk";
import { createPublicClient, createWalletClient, http, parseUnits, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { monadTestnet } from "viem/chains";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Set ${name}`);
  return value;
}
const account = privateKeyToAccount(required("PRIVATE_KEY") as `0x${string}`);
const transport = http(required("RPC_URL"));
const publicClient = createPublicClient({ chain: monadTestnet, transport });
const walletClient = createWalletClient({ account, chain: monadTestnet, transport });
if (await publicClient.getChainId() !== 10143) throw new Error("Expected Monad testnet");
// Dedicated tutorial owner; do not run another trading process for this account.
const accountCore = "0xe08573a1Ecc4aa507c9626071fA0c4513caf7906";
const market = "0xD2F64EaF7E85688802d24C18ea268C365533b344"; // MON/USDC
const token = "0xb01DC575a7ef75e58A05D2bde73ab79f8e7dd0Be"; // Test USDC
const kuru = createKuruClient({ publicClient, walletClient, account, addresses: { accountCore } });
async function confirmed(hash: Hash) {
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") throw new Error(`Transaction reverted: ${hash}`);
  return receipt;
}
function scaleDecimals(scale: bigint): number {
  if (!/^10*$/.test(scale.toString())) throw new Error("Expected power-of-ten precision");
  return scale.toString().length - 1;
}
const [priceScale, sizeScale, tick, minQuote, maxQuote] = await publicClient.readContract({ address: market, abi: abi.orderBookAbi, functionName: "getMarketParams" });
const price = parseUnits(required("LIMIT_PRICE"), scaleDecimals(BigInt(priceScale)));
const quantity = parseUnits(required("BASE_QUANTITY"), scaleDecimals(sizeScale));
const decimals = await publicClient.readContract({ address: token, abi: abi.erc20MetadataAbi, functionName: "decimals" });
const amount = parseUnits(required("DEPOSIT_USDC"), decimals);
if (price <= 0n || price > 0xffff_ffffn || price % BigInt(tick) !== 0n || quantity <= 0n || quantity >= (1n << 96n) || amount <= 0n) throw new Error("Invalid price, tick, quantity or deposit");
const quote = (price * quantity / sizeScale) * 10n ** BigInt(decimals) / BigInt(priceScale);
if (quote < minQuote || quote > maxQuote) throw new Error("Order outside market notional bounds");
let userId = await kuru.account.getRootAccountId({ rootOwner: account.address });
if (userId !== 0n) {
  const ids = await Promise.all(Array.from({ length: 62 }, (_, slotIdx) => kuru.spot.getOrderId({ market, userId, slotIdx })));
  if (ids.some(id => id !== 0n)) throw new Error("Use an account without existing orders in this market");
}
await confirmed(await kuru.account.approveErc20({ token, spender: accountCore, amount }));
await confirmed(await kuru.account.depositToOwner({ rootOwner: account.address, token, amount }));
userId = await kuru.account.getRootAccountId({ rootOwner: account.address });
const receipt = await confirmed(await kuru.spot.batch({
  market, userId,
  orders: [{ side: NativeSide.BUY, quantity, price, tif: NativeTif.GTC, executionInstruction: NativeExecInstruction.POST_ONLY, minSizeAfterBlock: 0n }],
  cancelSlotIdxs: [],
}));
// Decode only market logs; a successful post-only action can still place no order.
for (const log of receipt.logs) {
  if (log.address.toLowerCase() !== market.toLowerCase()) continue;
  const event = decodeKuruEventLog(log);
  if (event.eventName === "BookUpdatesPacked") {
    const args = event.args as unknown as { packedUpdates: `0x${string}` };
    console.log(decodeBookUpdatesPacked(args.packedUpdates));
  }
}
// This dedicated account began with no live orders; inspect all slots instead of assuming slot zero.
for (let slotIdx = 0; slotIdx < 62; slotIdx++) {
  const orderId = await kuru.spot.getOrderId({ market, userId, slotIdx });
  if (orderId === 0n) continue;
  await confirmed(await kuru.spot.cancelBySlots({ market, userId, cancelSlotIdxs: [slotIdx] }));
}
console.log({ accountId: userId, free: await kuru.account.getBalance({ accountId: userId, token }), reserved: await kuru.account.getSpotReservedBalance({ accountId: userId, token }) });
```

## What to check

Every transaction must have a successful receipt. A successful post-only submission does not guarantee an open order: inspect operation outcomes and current slots. The script scans all 62 slots rather than assuming the first order occupies slot zero.

Cancellation releases only the remaining reservation; it cannot reverse fills. Funds remain inside AccountCore after the tutorial. Use [Accounts and Funding](/sdk/accounts) to withdraw them, subject to current withdrawal checks.

For concurrent production workers, bind cancellations to live order IDs using the wallet's signed action flow, and follow [Signer Lifecycle](/sdk/signer-lifecycle). This tutorial's slot cleanup relies on exclusive use of its dedicated account.


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