Sign Offline
client.signer signs every Lighter transaction type locally, with no network access and no
nonce bookkeeping: you pass the nonce, it returns a SignedTx. Use it to pre-sign
transactions, to sign on one machine and submit from another, or to build batches
(Batch Transactions).
Sign Without Sending
Building a client opens no connection, so Lighter.new(...) with API keys is enough to
sign. Each client.tx transaction method has a signer twin of the same name, taking the
same arguments in the same shapes, plus an explicit nonce. The exceptions are
fast_withdraw and lit_lease: they submit to their own REST endpoints rather than
sendTx, so client.signer has no twin for them: send them through client.tx only.
from typed_lighter import Lighter
client = Lighter.new() # LIGHTER_ACCOUNT_INDEX, LIGHTER_API_KEY_INDEX, LIGHTER_API_PRIVATE_KEY
async with client:
key = client.signer.api_key_indices[0]
next_nonce = await client.api.account.keys.next_nonce(
account_index=client.signer.account_index, api_key_index=key
)
signed = client.signer.create_order(
{
'order_type': 'limit',
'market_index': 0,
'client_order_index': 1001,
'base_amount': 500,
'is_ask': False,
'price': 200_000,
'time_in_force': 'post-only',
},
nonce=next_nonce['nonce'],
api_key_index=key,
)
print(signed.tx_type, signed.tx_hash) # the hash Lighter will report
print(signed.tx_info) # the signed body, as JSONSigning itself needs no connection; only the nonce comes from Lighter. It has to be the
key's next nonce, exactly: api.account.keys.next_nonce as above, or
client.tx.reserve_nonces(...) when the same client also sends transactions on that key
(Batch Transactions), so the two never hand out the same one.
The hash is known before anything is sent. Submit the transaction with
client.tx.send(signed), or post tx_type and tx_info to Lighter's sendTx yourself,
before it expires: a signed transaction carries an expires_at, 10 minutes after signing
by default, after which Lighter refuses it. Pass expires_at= (a timezone-aware
datetime) to sign for longer.
client.tx.create_order(order) and client.signer.create_order(order, nonce=...) take the
same order, so code switches between sending now and signing ahead without reshaping
anything; the same holds for grouped orders, cancel-all and integrator approvals.
Every twin also takes api_key_index (the first configured key by default), expires_at
(when Lighter stops accepting the transaction; 10 minutes from signing by default; a naive
datetime raises BadRequest), and
skip_nonce, which lets Lighter accept any higher nonce instead of exactly the next one.
Without A Client
The signer stands alone too. Pick the chain id of the network the transaction is for; it is part of every transaction hash:
import os
from typed_lighter import AccountSigner
from typed_lighter.core import NETWORKS
signer = AccountSigner.new(
account_index=int(os.environ['LIGHTER_ACCOUNT_INDEX']),
api_keys={4: os.environ['LIGHTER_API_PRIVATE_KEY']},
chain_id=NETWORKS['mainnet'].chain_id,
)
nonce = int(os.environ['NEXT_NONCE']) # key 4's next nonce, fetched where there is a connection
cancel = signer.cancel_all_orders({'mode': 'immediate'}, nonce=nonce) # expires in 10 minutes
token = signer.auth_token() # a 10-minute auth token for REST reads and private streamsThe nonce still has to be the key's next one, read from GET /api/v1/nextNonce
(api.account.keys.next_nonce) wherever there is a connection, and the transaction has to
reach Lighter before its expires_at.
Keys And Tokens
from datetime import datetime, timedelta, timezone
from typed_lighter import Lighter
from typed_lighter.core.signer import generate_api_key
key = generate_api_key() # fresh key pair: register it with change_api_key(key.private_key, ...)
client = Lighter.new()
token = client.signer.auth_token(expires_at=datetime.now(timezone.utc) + timedelta(hours=8))A token carries only its expiry: it is valid from the moment it is signed until then, and
Lighter rejects any token whose expiry is more than 8 hours ahead
(20013 invalid auth: invalid deadline), so a token cannot be signed now for a later
window. auth_token raises BadRequest for such an expiry before signing. For access
that outlives 8 hours without an API key, use a read-only token
(Authenticated Setup).
Custom Signing Backends
The only secret-holding part is the Signer protocol: a public_key and a sign method
over a 40-byte hash. Pass any implementation in place of a hex key, for example one backed
by a hardware module or a remote signing service, and every transaction type works
unchanged:
from typed_lighter import Lighter, PythonSigner
client = Lighter.new(account_index=476, api_keys={4: PythonSigner.generate()})Performance And Side Channels
Signing is pure Python: a transaction or token takes a few milliseconds on a typical machine. It is not constant-time: someone able to time many signatures on the same machine could in principle learn about the key. Signature nonces are hedged (fresh randomness mixed with the key and message), so a weak random source alone does not leak the key. Keep signing on hardware you control.
Upstream reference: API keys.