> For the complete documentation index, see [llms.txt](https://docs.lighter.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lighter.xyz/developers/getting-started/quickstart-typescript.md).

# Quickstart: TypeScript + WASM

Sign and send your first Lighter order from TypeScript using the official WASM signer from lighter-go. Works in Node.js and the browser.

Lighter transactions are signed with a custom scheme (not EIP-712). The reference signer is written in Go and compiled to **WebAssembly**, so you can use it from any JavaScript runtime.

By the end of this page you will have one runnable script that verifies your API key, places a limit order on mainnet from Node.js and cancels it. Using Python instead? See [Get Started](/developers/readme.md).

## Prerequisites

* Node.js 22+ and Go 1.23+
* A Lighter account (deposit once via [app.lighter.xyz](https://app.lighter.xyz) or see [Deposits, Transfers and Withdrawals](/developers/integration-guides/deposits-transfers-and-withdrawals.md))
* An API key registered to that account: its **private key**, **API key index** (4–254), and your **account index**. Create one at [app.lighter.xyz](https://app.lighter.xyz) or programmatically – see [Create accounts programmatically](/developers/integration-guides/create-accounts-programmatically.md).

## 1. Build the signer

```shell
git clone https://github.com/elliottech/lighter-go
cd lighter-go
GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/

# Go's JS glue file, from your Go install
SRC="$(go env GOROOT)/lib/wasm/wasm_exec.js"
[ -f "$SRC" ] || SRC="$(go env GOROOT)/misc/wasm/wasm_exec.js"
cp "$SRC" ./build/wasm_exec.js
```

{% hint style="info" %}
**Build both files together**

`wasm_exec.js` must come from the **same Go version** that built the `.wasm`. Rebuild both together.
{% endhint %}

## 2. Set up the project

```shell
mkdir lighter-quickstart && cd lighter-quickstart
cp ../lighter-go/build/lighter-signer.wasm ../lighter-go/build/wasm_exec.js .
echo '{ "type": "module" }' > package.json
```

## 3. The script

Save as `quickstart.ts`. It verifies your API key, places a buy order far below the market (so it won't fill), then cancels it.

{% code title="quickstart.ts" %}

```typescript
import fs from "node:fs";
import "./wasm_exec.js"; // defines globalThis.Go

// ---------- config ----------
const BASE_URL = process.env.LIGHTER_URL ?? "https://mainnet.zklighter.elliot.ai";
const CHAIN_ID = Number(process.env.LIGHTER_CHAIN_ID ?? 304); // 300 for testnet
const ACCOUNT_INDEX = Number(process.env.LIGHTER_ACCOUNT_INDEX);
const API_KEY_INDEX = Number(process.env.LIGHTER_API_KEY_INDEX);
const API_PRIVATE_KEY = process.env.LIGHTER_API_PRIVATE_KEY!;

// ---------- load the signer ----------
const go = new (globalThis as any).Go();
const { instance } = await WebAssembly.instantiate(
  fs.readFileSync("./lighter-signer.wasm"),
  go.importObject,
);
go.run(instance); // do not await: the Go runtime stays alive
const lighter = globalThis as any;

function check<T extends { error?: string }>(res: T): T {
  if (res?.error) throw new Error(res.error);
  return res;
}

// ---------- HTTP helpers (all network calls go through JS fetch) ----------
async function get(path: string, headers: Record<string, string> = {}) {
  return fetch(`${BASE_URL}${path}`, { headers }).then(r => r.json());
}

async function sendTx(tx: { txType: number; txInfo: string }) {
  const r = await fetch(`${BASE_URL}/api/v1/sendTx`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({ tx_type: String(tx.txType), tx_info: tx.txInfo }),
  }).then(r => r.json());
  if (r.code !== 200) throw new Error(`sendTx ${r.code}: ${r.message}`);
  return r;
}

async function nextNonce(): Promise<number> {
  const r = await get(`/api/v1/nextNonce?account_index=${ACCOUNT_INDEX}&api_key_index=${API_KEY_INDEX}`);
  return r.nonce;
}

// ---------- 1. create the signer client (no network) ----------
check(lighter.CreateClient("", API_PRIVATE_KEY, CHAIN_ID, API_KEY_INDEX, ACCOUNT_INDEX));

// ---------- 2. verify the key is registered ----------
// An auth token signed by this key is accepted only if the key is registered for the account.
const { authToken } = check(lighter.CreateAuthToken(0, API_KEY_INDEX, ACCOUNT_INDEX));
const limits = await get(`/api/v1/accountLimits?account_index=${ACCOUNT_INDEX}`, { Authorization: authToken });
if (limits.code !== 200) throw new Error(`API key not registered for this account: ${limits.code} ${limits.message}`);
console.log("API key OK, tier:", limits.user_tier);

// ---------- 3. market decimals ----------
const books = await get("/api/v1/orderBookDetails");
const eth = books.order_book_details.find((m: any) => m.symbol === "ETH");
const toInt = (value: number, decimals: number) => Math.round(value * 10 ** decimals);

// ---------- 4. sign and send a limit order ----------
const clientOrderIndex = Date.now() % 2 ** 32;
const order = check(lighter.SignCreateOrder(
  eth.market_id,
  clientOrderIndex,
  toInt(0.01, eth.size_decimals),  // 0.01 ETH
  toInt(1000, eth.price_decimals), // $1,000: far from market, won't fill
  0,                               // isAsk: 0 = buy
  0,                               // orderType: limit
  1,                               // timeInForce: good-till-time
  0, 0,                            // reduceOnly, triggerPrice
  Date.now() + 28 * 24 * 3600e3,   // orderExpiry
  0, 0, 0,                         // integrator fields
  0, 0,                            // self-trade modes
  0, await nextNonce(),            // skipNonce, nonce
  API_KEY_INDEX, ACCOUNT_INDEX,
));
console.log("order accepted:", await sendTx(order));

// ---------- 5. cancel it ----------
const cancel = check(lighter.SignCancelOrder(
  eth.market_id, clientOrderIndex, 0, await nextNonce(), API_KEY_INDEX, ACCOUNT_INDEX,
));
console.log("cancel accepted:", await sendTx(cancel));

process.exit(0); // the Go runtime keeps Node alive otherwise
```

{% endcode %}

Notes on what it does:

* **Keep the WASM signer off the network.** `CreateClient` gets an empty URL and every request goes through JS `fetch`. Avoid `CheckClient` and `nonce = -1` (which asks the signer to fetch the nonce itself): both use Go's HTTP stack, which can't reach the network from WASM (you'll see `dial tcp: lookup …`).
* **Key check:** an auth token signed by your key is only accepted if that key is registered for the account. Any non-200 code (e.g. `invalid signature`, `couldnt find account`) means the key, key index or account index is wrong.
* **Integers:** prices and sizes are integers scaled by the market's `price_decimals` / `size_decimals` from `orderBookDetails`. See [Core concepts](/developers/getting-started/core-concepts.md#integer-prices-and-sizes).
* **Every** function returns an object; if it has an `error` field, the call failed. `check()` turns that into an exception.

## 4. Run it

```shell
export LIGHTER_ACCOUNT_INDEX=123
export LIGHTER_API_KEY_INDEX=4
export LIGHTER_API_PRIVATE_KEY=0x...
# testnet: export LIGHTER_URL=https://testnet.zklighter.elliot.ai LIGHTER_CHAIN_ID=300

npx tsx quickstart.ts
```

Expected output:

```
API key OK, tier: std
order accepted: { code: 200, tx_hash: '...', predicted_execution_time_ms: ..., ... }
cancel accepted: { code: 200, tx_hash: '...', ... }
```

{% hint style="warning" %}
**Accepted is not executed**

`code: 200` means the API **accepted** the transaction. It can still be rejected by the sequencer (e.g. insufficient margin). Confirm with the `account_orders` / `account_tx` WebSocket channels. See [Account data & positions](/developers/integration-guides/account-data.md).
{% endhint %}

## Reusing the signer in your app

The guides import the loader from a small module. Split it out of the script like this:

{% code title="signer.ts" %}

```typescript
import fs from "node:fs";
import "./wasm_exec.js";

const go = new (globalThis as any).Go();
const { instance } = await WebAssembly.instantiate(fs.readFileSync("./lighter-signer.wasm"), go.importObject);
go.run(instance);

export const lighter = globalThis as any;
export function check<T extends { error?: string }>(res: T): T {
  if (res?.error) throw new Error(res.error);
  return res;
}
```

{% endcode %}

## Running in the browser

Same code, but load the module with `fetch`:

```typescript
await import("/wasm_exec.js");
const go = new (window as any).Go();
const { instance } = await WebAssembly.instantiateStreaming(
  fetch("/lighter-signer.wasm"), go.importObject,
);
go.run(instance);
```

Serve the `.wasm` with `Content-Type: application/wasm`. Pre-compressing with Brotli/Gzip is recommended. The same rules apply as in Node: pass an empty URL to `CreateClient` and make HTTP calls with `fetch`.

{% hint style="danger" %}
**Never ship your own API key to a browser**

In a frontend, generate a key **per user** on their device and register it with their wallet. See [Create accounts programmatically](/developers/integration-guides/create-accounts-programmatically.md).
{% endhint %}

## Next steps

* [Signing Transactions](/developers/integration-guides/signing-transactions.md) – market orders, TP/SL, modify, batch
* [Market data](/developers/integration-guides/market-data.md) – stream order books and trades
* [WASM signer functions](/developers/reference/wasm-signer.md) – all 20+ functions and their arguments
* Looking for a complete TypeScript client state store and WebSocket pipeline? See [elliottech/lighter-ts](https://github.com/elliottech/lighter-ts).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.lighter.xyz/developers/getting-started/quickstart-typescript.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
