/core/v1/lit_action endpoint with any required js_params. The pkpId parameter is the wallet address of the PKP you want to use, passed in via js_params.
For examples that need more than one file to run — a Solidity contract, a deploy script, an off-chain client — see the examples/ folder in the repo.
1. Sign a Message
The simplest pattern: retrieve a PKP’s private key and sign an arbitrary message with it. The signature proves the message was attested by a specific, on-chain-registered key.2. Encrypt a Secret
Encrypt a sensitive string so that only the holder of the PKP can later decrypt it. Useful for storing API keys, passwords, or personal data on-chain or in IPFS without exposing the plaintext.ciphertext anywhere — IPFS, a smart contract, a database — and retrieve the plaintext only when needed from an action that is permitted to use the same PKP.
3. Decrypt a Secret
Decrypt a ciphertext that was previously produced byLit.Actions.Encrypt using the same PKP. Only an action that is permitted to use the PKP (enforced on-chain) can decrypt it.
4. Fetch a Crypto Price and Sign It
Fetch the current price of ETH from a public API and sign the result. The caller receives both the price and a signature — a verifiable price proof that can be submitted to a smart contract as a trusted oracle update.ecrecover on the signature to confirm the price was signed by a specific, known PKP address — without trusting any off-chain intermediary.
5. Gate a Signature on Live Weather Data
Fetch live weather for a city using a decrypted API key and only sign a message if the temperature exceeds a threshold. Demonstrates combining decryption, an authenticated HTTP request, and conditional signing in one action.6. Read from a Smart Contract
Call a view function on an EVM smart contract and return the result. Useful for reading on-chain state (balances, governance votes, NFT ownership) inside an action, or for gating downstream logic on chain data.7. Send ETH to an Address
Construct, sign, and broadcast an ETH transfer transaction from a PKP wallet. The PKP pays the gas and the transfer amount, so ensure the PKP wallet holds sufficient ETH on the target chain before running this action.8. Gate an ERC-20 Transfer on On-Chain Sanctions Data (Cross-Chain)
Screen the recipient of every transfer against the Chainalysis on-chain sanctions oracle and only sign a transfer authorization when the recipient is clear. The Chainalysis oracle is free and keyless — it’s just a smart contract at0x40C57923924B5c5c5455c48D93317139ADDaC8fb you can staticcall. But it is only deployed on a handful of mainnets (Ethereum, Arbitrum, Polygon, BSC, Avalanche, Optimism, Celo). On Base, Linea, Scroll, any L3, any testnet, or any non-EVM chain, a contract can’t reach it. The Lit Action bridges that gap: it eth_calls the oracle on Ethereum mainnet, then signs an authorization that the CompliantToken contract — deployed wherever you want — verifies with ecrecover.
The signature uses Lit.Actions.getLitActionPrivateKey() — an identity derived from the action’s IPFS CID. See Action-Identity Signing.
The trust anchor is a hardcoded hostname whitelist. Anyone calling the action supplies screeningRpcUrl via js_params, so a caller-supplied chainId check would just be theater (pair a malicious RPC with a matching chain id, gate passes). Instead the action checks the URL’s hostname against eth-mainnet.g.alchemy.com — TLS guarantees we’re actually talking to Alchemy. Trust shifts to “Alchemy is honest about Ethereum mainnet.” See Hostname-Pinned RPC Trust Anchors.
Lit.Actions.getLitActionWalletAddress({ ipfsId }) from inside any helper action, then pass that address to the CompliantToken constructor.
Swapping providers (Infura, QuickNode, your own node) means editing the regex — which produces a new action CID and signer address, requiring a redeploy. That’s by design: the trust anchor is content-addressed. For richer screening — hacker wallets, mixer interactions, fresh threat intel — swap the on-chain lookup for a paid API like Chainalysis KYT, TRM Labs, or GetBlock. The pattern becomes: encrypt the API key to a PKP, decrypt inside the TEE, call the API, sign on pass.
The matching contract signs nothing itself — it just verifies that the digest recovers to a hard-coded PKP address:
transfer and transferFrom overrides revert, so every movement of tokens must go through this gate.
The full runnable example — token contract, hardhat deploy script, and an end-to-end transfer runner — lives at
examples/compliance-transfer-gate/ in the repo. The example is keyless: the action reads the Chainalysis oracle via an Alchemy RPC and signs with its own CID-derived key, so no PKP or encrypted secrets are required.9. Median Price Oracle Across Three Exchanges
Fetch a spot price from three independent exchanges (Coinbase, Kraken, Bitstamp), take the median, and sign it for any EVM chain. This is the practical “I need a Chainlink-shaped feed without Chainlink” pattern. Median (rather than strict byte-equality) is the right aggregation for live market prices — exchanges disagree by a few cents at every moment, so byte-equality would never pass. A median naturally rejects one outlier; combined with aMAX_SPREAD_BPS check (refuse to sign if min/max differ by more than the threshold) it catches both single-source manipulation and any-source-market-state-broken situations.
The safety thresholds (MAX_SPREAD_BPS, MIN_SOURCES, DECIMALS) are hardcoded constants in the action source rather than caller-supplied js_params. Otherwise anyone holding the usage key could request a signature with MIN_SOURCES: 1 and a huge spread cap, bypassing the median-of-three story. Editing a constant mints a new action CID — and therefore a new signer address — which forces a redeploy of the registry. The trust anchor is content-addressed.
All three sources here are keyless public HTTP endpoints — no API keys, no PKP, no encryption.
The full runnable example —
PriceOracle registry contract, deploy script, end-to-end submission runner, and a zero-dep npm run test-medianizer harness that exercises the fetch logic without touching any chain — lives at examples/multi-source-price-oracle/ in the repo.10. Resolve a Prediction Market by AI Consensus
Poll multiple LLM providers in parallel with the same yes/no question and only sign the resolution when every model agrees. This uses the Multi-Source Consensus pattern with AI providers as the parallel sources. The aggregation is strict agreement rather than a median, because the output is categorical YES/NO/UNCLEAR. Perplexity Sonar is required because its built-in web search lets it answer questions about events that happened after a frontier model’s training cutoff. OpenAI and Anthropic are optional second opinions — independent training corpora mean a confident-but-wrong frontier answer is unlikely to be confirmed by another frontier model. Configuring all three gives you 3-of-3 agreement before anything reaches the chain.The full runnable example —
PredictionMarket contract, deploy script, key-encryption helper, propose/resolve runners, and a heavily-commented setup pipeline — lives at examples/prediction-market-oracle/ in the repo.11. Cross-Chain Burn/Mint Bridge
Deploy the sameBridgeToken contract on two chains. The holder calls burn on chain A, which destroys the local supply and emits BurnInitiated(from, recipient, amount, destChainId, nonce). A Lit Action reads that event via eth_getTransactionReceipt against a hostname-whitelisted RPC, validates it, and signs a mint authorization for chain B. Anyone can submit the mint — the signature is the authorization, not the caller.
The signer key comes from Lit.Actions.getLitActionPrivateKey(), which derives the key from the action’s IPFS CID. Edit the action by a byte and the signer changes, and every deployed BridgeToken refuses the modified action. The trust collapses from “trust this federation of relayers” to “trust this exact piece of code.” See Action-Identity Signing and Hostname-Pinned RPC Trust Anchors.
BridgeToken.mint re-derives the same digest, recovers the signer, and checks it matches the pinned bridgeOracle. It also checks an independent bridgePartner[srcChainId] mapping — wired during setup to point at the sibling deployment — so a forged burn from a copycat contract with the same event shape can’t mint here. Each (srcChainId, burnTxHash, logIndex) is recorded in usedBurnIds to prevent replays.
This is the permissionless half: any wallet can submit the mint tx (sponsored by a relayer, the recipient themselves, or whoever wants the gas burden). The mint goes through only because the signature is valid — there’s no on-chain allowlist of submitters.
The full runnable example —
BridgeToken contract, two-chain deploy script, setBridgePartner wiring, and an end-to-end npm run bridge runner that burns on one chain and mints on the other — lives at examples/cross-chain-token/ in the repo. Defaults to Base Sepolia ↔ Arbitrum Sepolia; the RPC_HOSTS table is the only thing you’d touch to add more chains.12. Policy-Gated Key Custody for a Solver / Filler
Intent-system solvers and fillers (UniswapX, Across, CoW, 1inch Fusion, ERC-7683, bridge relayers) run a bot that holds a hot key and signs fills against an inventory balance. Compromise the box, drain the inventory. This example removes the hot key: inventory lives in aSolverVault contract, and the only signature that releases a fill comes from a Lit Action that is the policy. The bot can ask Lit to authorize a fill; it can’t authorize one itself.
The action reads the real order/deposit from a pinned, trusted settlement contract on-chain and reconstructs the fill from it — so the recipient and amount come from what the order actually says, not from anything the (possibly compromised) caller supplies. Only then does it sign, using its CID-derived identity (see Action-Identity Signing). Edit the policy and the signer address changes, so the vault stops trusting the modified action.
The full runnable example —
SolverVault / AcrossSolverVault contracts, the policy actions, attacker scripts that prove exfiltration is impossible, an exit() cold-wallet path, and a live Across testnet relayer (deposit on Sepolia → Lit authorizes → vault fills on Base Sepolia, ~355 ms round-trip), plus a read-only ops dashboard — lives at examples/lit-solver-vault/ in the repo.13. Compliant Private Stablecoin
Every public stablecoin (USDC, USDT, PYUSD) puts your whole financial life on a public ledger: payroll, vendor payments, who paid whom and how much, forever. Shielded pools (Zcash, Aztec) fix the privacy but have no compliance story, so issuers won’t touch them. This example is the missing middle — private by default, compliant by construction — and it gets there without a ZK circuit. Balances aren’t a public mapping; a wallet’s balance is a set of notes ({owner, amount, salt}). On-chain you only ever see a note’s commitment (keccak256(owner, amount, salt)), its nullifier when spent, and its contents encrypted to a ledger PKP (decryptable only inside an authorized Lit Action). A private transfer publishes new commitments + a nullifier + ciphertext — no amount, no parties. The Lit Action plays the role a ZK circuit plays in Zcash/Aztec: it reads chain state over a pinned RPC, validates the input notes exist and are unspent, checks sum(inputs) == sum(outputs), runs OFAC screening (the sanctions gate from §8), then signs the state update with its CID-derived identity (see Action-Identity Signing). KYC runs only at the dollar edges (mint/redeem), reserves are publicly provable (usdc.balanceOf(vault) ≥ totalSupply()), and a regulator holding a threshold-signed warrant can decrypt exactly one note while every other balance stays dark.
The full runnable example — the
PrivUSD contract (commitments, nullifiers, encrypted blobs, reserve proof), the ledger action (mint/transfer/redeem prover with OFAC + KYC baked in), a warrant-gated disclose action, a 2-minute scripted demo, and a Hardhat test suite — lives at examples/private-stablecoin/ in the repo. It builds on the sanctions gate (§8) and runs live on Base Sepolia.14. A Unique, Immutable Wallet Per User (Bound to the Action)
A common request: “bind a wallet to an action immutably, and give each user their own.” You can do this with a ChainSecured account and a contract that mints and binds PKPs to a group — but there’s a lighter pattern that gets the same “only this exact code can sign” property with no PKP and no contracts. Every action has a key derived from its IPFS CID viagetLitActionPrivateKey() — so the wallet is bound to the code. To get a different wallet per user, make the code different per user: hardcode the user’s address into the action. That one line is part of what the CID hashes, so each user gets a different CID and therefore a different, immutable wallet. Authorize spending by recovering a signature inside the action and comparing it to the hardcoded owner — the usage key that runs the action grants no spending power.
The full runnable example — the per-user action template, a
DemoToken ERC-20, a one-shot setup, deposit/balance/withdraw scripts, and a wrong-user attack that the action refuses — lives at examples/action-bound-wallet/ in the repo.15. Confidential Dark Pool (Encrypted Orders, Matched in the Enclave)
A dark pool is a venue where orders stay hidden until they match, so large orders can’t be front-run. Every other example on this page is compute + sign; this one is the first to use encryption and to hold confidential state, which is what a dark pool actually needs. Traders submit orders encrypted to a vault PKP; they’re stored as ciphertext in ordinary Postgres (so the database operator only ever sees gibberish — even the DB connection string is an encrypted secret the action decrypts at runtime); at the end of each epoch a Lit Action decrypts the whole batch inside the TEE, runs a single-clearing-price sealed-bid auction, and signs the resulting fills for an on-chain settlement contract that pins the action’s CID-derived address. It’s a sealed-bid batch auction, not a continuous order book, on purpose: a uniform clearing price removes time-priority, so there’s no ordering advantage to front-run, and the whole batch matches in one atomic enclave run (no per-call sequencing on top of stateless actions). Each order is signed by its trader and the matcher verifies that signature in-enclave, so nobody — not even the operator holding the usage key — can forge an order against someone else’s escrow.Privacy here is enforced by the TEE: orders are decrypted only inside an attested enclave and are never exposed to the operator, the database, or other traders — the book is matched in hardware isolation, with true async batching. The privacy is pre-trade — orders are hidden until they match; settled fills are public on-chain, like a real dark pool’s trade reporting. See the example’s “Security model & limitations” for the full trust picture, including what trusting the enclave entails.
The full runnable example —
DarkPoolSettlement (per-epoch locked escrow + signed-fill settlement) and TestToken contracts, the encryptOrder / matchEpoch / markSettled actions, a setup that mints the vault PKP, pins the action CIDs, encrypts the DB connection string, and deploys the contracts, plus trader-signed submit and epoch run scripts and a Hardhat test suite (contract + auction + order-authentication) — lives at examples/dark-pool/ in the repo. It uses Neon Postgres because a Lit Action reaches the DB over HTTP, not a raw socket.16. Non-Custodial Co-Signer: Threshold ECDSA Split Between Lit and You
Every other example on this page is a “Lit signs on your behalf” flow: the action holds a key and can sign whenever its code decides to. This one is the first where you are a required co-signer. The signing key is real threshold ECDSA (the DKLs23 protocol, via Silence Laboratories’ Trail-of-Bits-audited WASM), split 2-of-3 by a distributed key generation: Lit holds one share, you hold a hot share (this machine) and a cold recovery share (offline). Any two shares can sign. Lit literally cannot produce a signature without you co-signing, and the full private key is never assembled anywhere — not even momentarily inside the action. The output is a standard secp256k1 ECDSA signature any EVM contract verifies with plainecrecover.
Because you hold 2 of the 3 shares, the cold share is a self-custody escape hatch: if Lit ever disappears you sign with hot + cold entirely client-side (no Lit, no network), so funds never freeze. The action is stateless across calls — each round it seals its MPC session with Lit.Actions.Encrypt({ pkpId }) and the user relays that opaque blob back the next round; neither party’s share alone can sign.
The full runnable example — the stateless
mpcSigner action, a user-side client + local share store, MpcVault.sol (verifies the MPC signature with plain ecrecover), setup / keygen / deploy / sign scripts covering the interactive DKG and both signing quorums (hot + Lit, and the no-Lit hot + cold recovery path) — lives at examples/mpc-signing-ecdsa/ in the repo. Verified end-to-end on Base Sepolia; npm run keygen -- --basic does a simpler 2-of-2 (Lit + hot, no recovery).17. Sign a Solana Transaction (Keyless ed25519 Wallet)
Every other signing example on this page targets an EVM chain, where the action’s identity key is already a secp256k1 EVM key. Solana uses ed25519, so this example shows the small bridge: that identity key is a 32-byte secp256k1 private key, and 32 bytes is exactly an ed25519 seed — which is what Solana’sKeypair.fromSeed consumes. So you can derive a Solana keypair from the action’s CID-bound identity, giving a keyless Solana wallet that only this exact code can ever operate — no PKP to mint, no key to hold.
The action also inspects what it signs: rather than blindly signing whatever bytes it’s handed, it parses the serialized transaction message and signs only a single SystemProgram transfer whose fee payer is its own address and whose amount is under a hardcoded cap. The canonical message bytes are built client-side by @solana/web3.js; the parse inside the action is read-only validation, so a parser quirk can only ever reject — it can never sign something other than the exact bytes the client broadcasts. ed25519 + base58 come from pinned ESM imports (jsDelivr).
The full runnable example — the policy-enforcing
solanaSigner action (with a from-scratch legacy-message parser), a one-shot setup that derives the wallet’s Solana address, and airdrop / transfer client scripts that fund the wallet and round-trip a signed transfer on devnet (including an over-cap send the action refuses) — lives at examples/solana-signer/ in the repo.