How to write wallet signing policies as code
On Lit, a signing policy is a JavaScript function. It runs inside the enclave with the key, so it can check whatever it needs before it signs. Here is how to write one and attach it to a wallet.
On Lit Protocol, a signing policy is a JavaScript function called a Lit Action. It runs inside the same secure enclave that holds the key. Before it signs, it can call any API, read any chain, and check any condition you want. If the check fails, it does not sign. You attach the function to a wallet by its content hash, and that permission is recorded on Base.
I want to explain why we built it this way, and then show you the code.
Most wallet providers let you set rules in a dashboard. You can allow certain addresses and cap the amount. Those rules can only look at the transaction itself. They cannot look at your ledger, a price feed, or a deposit on another chain. So if your rule is "only sign this settlement after the deposit has confirmed," you have to check that in your own server before you ask for the signature. That puts the check back in the place you were trying not to trust.
With a Lit Action, the check and the signature happen in the same place. Your server cannot skip the check because your server never touches the key.
Write the policy
Here is a policy that signs a transfer only if the recipient passes a screening check and the day's spending stays under a cap.
// policy-transfer-v1.js
async function main({ pkpId, tx, recipient, amountUsd }) {
const screen = await fetch(`https://screening.example.com/check/${recipient}`).then(r => r.json());
if (!screen.clear) return { signed: false, reason: "recipient flagged" };
const ledger = await fetch(`https://ledger.example.com/spend/today?wallet=${tx.from}`).then(r => r.json());
if (ledger.spentUsd + amountUsd > 25000) return { signed: false, reason: "daily cap" };
const wallet = new ethers.Wallet(await Lit.Actions.getPrivateKey({ pkpId }));
return { signed: true, signedTx: await wallet.signTransaction(tx) };
}Both of those fetch calls run inside the enclave. The cap and the screening URL are part of the code. When you publish the file to IPFS, you get a content hash. That hash is the policy's identity. If you change anything in the file, you get a different hash, and the wallet will not run it until you approve it.
Attach it to a wallet
Lit uses groups to connect wallets, policies, and API keys. You add the policy and the wallet to the same group.
curl -s -X POST https://api.chipotle.litprotocol.com/core/v1/add_action_to_group \
-H "X-Api-Key: $ACCOUNT_KEY" -H "Content-Type: application/json" \
-d '{"group_id":1,"action_ipfs_cid":"<policy-transfer-v1 CID>"}'
curl -s -X POST https://api.chipotle.litprotocol.com/core/v1/add_pkp_to_group \
-H "X-Api-Key: $ACCOUNT_KEY" -H "Content-Type: application/json" \
-d '{"group_id":1,"pkp_id":"<pkp id>"}'Groups live in smart contracts on Base. If you switch your account to ChainSecured mode, a wallet you control owns the account, and every change to a group is a transaction you sign. Anyone can look at the contract and see which policies a wallet trusts.
Give your server a key that can only execute
curl -s -X POST https://api.chipotle.litprotocol.com/core/v1/add_usage_api_key \
-H "X-Api-Key: $ACCOUNT_KEY" -H "Content-Type: application/json" \
-d '{"name":"payouts","can_create_groups":false,"can_delete_groups":false,"can_create_pkps":false,
"manage_ipfs_ids_in_groups":[],"add_pkp_to_groups":[],"remove_pkp_from_groups":[],"execute_in_groups":[1]}'This key can run the policies in group 1 against the wallets in group 1. It cannot add a policy, add a wallet, or change the group. If someone steals it, the most they can do is request signatures that already pass your policy.
Change the policy later
Write a new version of the file and publish it. Add the new hash to the group and remove the old one. The wallet address stays the same. The record of which policies were allowed, and when, stays on Base.
A few things to keep in mind
Treat the policy like production code, because that is what it is. A bug in the cap check is a signed transaction. The services your policy calls are part of your trust model, so pin the hostnames and validate what comes back. And keep the wallet's balance at a working level, since a policy stops out-of-policy signatures but cannot undo a mistake that passed the policy.
Get started
You can create an account at dashboard.chipotle.litprotocol.com. The Lit Actions docs are at developer.litprotocol.com/lit-actions, and groups are covered at developer.litprotocol.com/architecture/groups.
This is part 1 of a five-part series on building with Lit. Part 2 comes out on Wednesday, September 30, and is about giving Claude Code and MCP servers secrets safely.