A single Python script that proves a Dynamic server wallet can drive the Lighter chain flow which requires an L1 signature — registering an L2 API key (ChangePubKey).
Lighter's other flows need nothing more than an L2 key and an /api/v1/sendTx
call, so once this one works, the rest follows: deposits are plain ERC-20
transfers on the EVM chain that the Dynamic wallet can do natively, and
withdrawals / orders / transfers use the L2 key this script registered.
- Create or load a Dynamic EVM server wallet (MPC — no raw key anywhere).
- Dynamic signs a plain demo message — the simplest compatibility check.
- Look up the wallet's Lighter account by L1 address.
- Generate an L2 API key in lighter-python's native signer.
- Ask the signer for the ChangePubKey
messageToSign, sign it with Dynamic (DynamicEvmWalletClient.sign_message— EIP-191 personal_sign via MPC), splice the signature intotx_info.L1Sig, and POST/sendTx. - Exercise the just-registered L2 key with a 0.01 USDC self-transfer (perp → spot), an L2-only call requiring no L1 signature.
Step 5 is the only step where Lighter cares about the L1 key holder, and
Dynamic's signature is bit-identical to what eth_account.Account.sign_message
would produce from a raw key — so lighter-python's signer accepts it.
Run from inside examples/python-lighter-demo/ with Python 3.11+:
cd examples/python-lighter-demo
python3.11 -m venv .venv
source .venv/bin/activate
pip install -e .
cp .env.example .env
# fill in DYNAMIC_API_TOKEN and DYNAMIC_ENV_IDPrerequisite: the L1 address must already have a Lighter account. If not, bridge a small amount of USDC to it via app.lighter.xyz (or testnet) first — the account is created on first deposit.
lighter-demo
# or: python3 main.pyExpected output:
[1] Created Dynamic wallet (ephemeral): 0x...
[2] Dynamic signed 'Hello from Dynamic! ...'
→ 0x...
[3] Lighter accountIndex: 12345 (https://testnet.app.lighter.xyz/account/12345)
[4] New L2 API key for slot 3
[5] ChangePubKey accepted: 0x...
→ https://testnet.zklighter.elliot.ai/api/v1/tx?by=hash&value=0x...
[6] Self-transfer 0.01 USDC perp→spot: 0x...
→ https://testnet.zklighter.elliot.ai/api/v1/tx?by=hash&value=0x...
Dynamic × Lighter: compatible ✓
Set DYNAMIC_WALLET_ADDRESS in .env to reuse an existing wallet across
runs instead of creating a fresh one each time.
- The L2 API key is generated in-process and discarded when the script exits. Re-running this rotates it (ChangePubKey invalidates the prior key at the same slot).
API_KEY_INDEX = 3— do not use slot 0 in production, it's reserved for the main Lighter web app session.- The L1 signature is scoped by Lighter's chain id (300 testnet / 304
mainnet) inside
messageToSign, so testnet signatures can't be replayed on mainnet. - Dynamic's MPC never exposes the raw private key to this process — the
signature comes back from
sign_messagealready assembled. - No secrets committed:
.envis gitignored,.env.examplehas no credentials.