> 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/evm/signing-core-transactions.md).

# Signing Core transactions

Applications can construct and sign transactions locally, then submit them directly to Core. Depending on the operation, authorization requires an API-key signature, the account controller's signature, or both. See [Accounts](/evm/interop/accounts.md#account-authorization) for the authorization model.

Lighter's [TypeScript and WASM quickstart](https://docs.lighter.xyz/developers/getting-started/quickstart-typescript) explains the JavaScript setup and signing workflow. Its [transaction guides](https://docs.lighter.xyz/developers/integration-guides/signing-transactions) and [WASM signer reference](https://docs.lighter.xyz/developers/reference/wasm-signer) cover the existing transaction types. This page follows the same workflow for the additional transaction types available on the devnet.

## Set up the signer

Follow Lighter's [TypeScript and WASM quickstart](https://docs.lighter.xyz/developers/getting-started/quickstart-typescript), but use the [`evm-devnet` branch of `lighter-go`](https://github.com/elliottech/lighter-go/tree/evm-devnet). It retains the existing signing methods and adds WASM exports for `SignSendToEvm` and `SignWithdrawToL1Address`.

Replace the quickstart's clone command with the following.

```bash
git clone --branch evm-devnet \
  https://github.com/elliottech/lighter-go.git lighter-go
cd lighter-go
```

Build the `./wasm/` target described in the quickstart and use the resulting `lighter-signer.wasm`, with `wasm_exec.js` from the same Go version. The new methods are not in upstream release binaries or the devnet preview version's `web-wasm` target.

The examples below use the quickstart's reusable [`signer.ts` module](https://docs.lighter.xyz/developers/getting-started/quickstart-typescript#reusing-the-signer-in-your-app) with the devnet preview WASM binary built above. The module exports `lighter` and `check`. `check` throws when a signer call returns an error. The [browser setup](https://docs.lighter.xyz/developers/getting-started/quickstart-typescript#running-in-the-browser) uses the same signing methods.

## Generate an API-key pair

With the signer set up, call [`GenerateAPIKey()`](https://docs.lighter.xyz/developers/reference/wasm-signer#client-and-keys) to generate a key pair locally.

```typescript
import { lighter, check } from "./signer";

const { privateKey, publicKey } = check(lighter.GenerateAPIKey());
```

Generating the pair does not require a wallet signature or submit a transaction. Before using the key to sign routine Core transactions, [register it on the Core account](/evm/interop/accounts.md#api-key-authorization).

## Initialize the signing client

Set `LIGHTER_URL` to your network's Core API URL and `LIGHTER_CHAIN_ID` to its **Core chain ID**, not its EVM or L1 chain ID. The devnet Core chain ID is `300`; see [Chain Info](/evm/chain-info.md). Do not keep the upstream quickstart's mainnet defaults.

Use the account index, API-key index, and API private key for a key already registered on that Core account.

```typescript
import { lighter, check } from "./signer";

const CORE_API_URL = process.env.LIGHTER_URL!;
const CORE_CHAIN_ID = Number(process.env.LIGHTER_CHAIN_ID);
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!;

check(lighter.CreateClient(
  "", API_PRIVATE_KEY, CORE_CHAIN_ID, API_KEY_INDEX, ACCOUNT_INDEX,
));

async function nextNonce(): Promise<number> {
  const res = await fetch(
    `${CORE_API_URL}/api/v1/nextNonce?account_index=${ACCOUNT_INDEX}&api_key_index=${API_KEY_INDEX}`,
  ).then(r => r.json());
  if (res.code !== 200) throw new Error(res.message);
  return res.nonce;
}
```

Keep HTTP requests in JavaScript and pass an explicit nonce to the signer. Use one Core chain ID per WASM instance. The bindings take JavaScript numbers for integer arguments, so keep them within `Number.MAX_SAFE_INTEGER`.

Core-assigned spot `orderIndex` values exceed this limit because they encode the market index above the low 48 bits. When placing a spot order, set a unique, nonzero `clientOrderIndex` for the account, then pass it as the order index to `SignCancelOrder(...)` or `SignModifyOrder(...)`. Client order indexes are at most `2^48 - 1`, so they fit safely in a JavaScript number.

## Sign the devnet transaction types

Choose one of the following calls. Each returns `txType`, `txInfo`, `txHash`, and `messageToSign`. `txInfo` is a JSON string containing the API-key signature. Signing does not submit the transaction.

### Send to EVM

This example signs a transfer of `0.01` ETH from the account's spot balance. Set `EVM_RECIPIENT` to the recipient and `LIGHTER_SEND_TO_EVM_FEE` to the fee required by your network, in [Core USDC base units](/evm/interop/tokens/token-transfers.md#amounts). See [Send to EVM](/evm/interop/tokens/token-transfers/evm-and-core.md#send-to-evm-from-core) for balance, fee, and authorization requirements.

```typescript
const tx = check(lighter.SignSendToEvm(
  1,                                           // assetIndex: ETH
  process.env.EVM_RECIPIENT!,                  // evmRecipient
  1,                                           // routeType: spot
  1_000_000,                                   // baseAmount: 0.01 ETH
  Number(process.env.LIGHTER_SEND_TO_EVM_FEE), // usdcFee
  await nextNonce(),
  API_KEY_INDEX,
  ACCOUNT_INDEX,
));
```

### Withdraw to L1 Address

This example signs a withdrawal of `0.01` ETH from the account's spot balance. Set `L1_RECIPIENT` to the recipient on L1. See [Core to L1](/evm/interop/tokens/token-transfers/l1-and-core.md#core-to-l1) for withdrawal requirements and finalization.

```typescript
const tx = check(lighter.SignWithdrawToL1Address(
  1,                         // assetIndex: ETH
  process.env.L1_RECIPIENT!, // l1Recipient
  1,                         // routeType: spot
  1_000_000,                 // baseAmount: 0.01 ETH
  await nextNonce(),
  API_KEY_INDEX,
  ACCOUNT_INDEX,
));
```

## Add controller authorization

The WASM signer returns `messageToSign`, which the account controller signs using EIP-191 personal-sign. The application then attaches the signature before submission. For these two new transaction types, the signature field is `OwnerSig` rather than `L1Sig`.

Withdraw to L1 Address always requires the EOA controller's signature. Send to EVM requires it unless the recipient's EVM-controlled Core account index is exactly the account being debited. Sending from a subaccount to its master's EVM address still requires the signature.

When the signature is required, collect and attach it as follows. Here, `walletClient` is a connected viem wallet client and `controllerAddress` is the Core master account's controlling EOA. Sign `messageToSign`, not `txHash`.

```typescript
const ownerSig = await walletClient.signMessage({
  account: controllerAddress,
  message: tx.messageToSign,
});

tx.txInfo = JSON.stringify({
  ...JSON.parse(tx.txInfo),
  OwnerSig: ownerSig,
});
```

For a Send to EVM transfer that does not require controller authorization, leave `txInfo` unchanged. A contract controller must use its [native-domain method](/evm/interop/accounts.md#master-authorization) when controller authorization is required.

## Submit to Core

Submit the signed JSON to the network's Core API using [`sendTx`](https://docs.lighter.xyz/api/transactions).

```typescript
const res = await fetch(`${CORE_API_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 (res.code !== 200) throw new Error(`sendTx ${res.code}: ${res.message}`);
```

API acceptance does not confirm successful execution on Core. Use Lighter's [transaction lookup](https://docs.lighter.xyz/api/transactions) to check the Core outcome. A transaction with `status` of `2` (executed) or `3` (packed) may still have failed during Core execution. Parse the JSON string in `event_info` and check its `ae` field, which is empty on success or contains a JSON-encoded error on failure. A transaction that fails this way still consumes its nonce, so fetch the next nonce before signing a retry.

For the new transaction types covered on this page, successful execution on Core is followed by delivery or finalization on the destination domain. Follow the corresponding guide after confirming success on Core.

* **Send to EVM.** See [EVM delivery](/evm/interop/tokens/token-transfers/evm-and-core.md#delivery-on-evm).
* **Withdraw to L1 Address.** See [L1 withdrawal finalization](/evm/interop/tokens/token-transfers/l1-and-core.md#finalizing-a-core-withdrawal).


---

# 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/evm/signing-core-transactions.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.
