How to sign transactions from your backend without exposing the private key
Keep the key out of your server. Put the rule that decides each signature next to the key. Here is how that works on Lit, with the code.
Keep the private key out of your backend entirely. Put it in a signer your backend calls, and put the rule that decides each signature next to the key instead of in your server. On Lit Protocol the key lives inside a secure enclave, and the rule is a Lit Action, a JavaScript function that runs in that enclave before every signature. Your backend gets an API key that can only request signatures. If someone breaks into your backend, they can ask for signatures that pass your rule, and that is all they can do.
The way most people start is with a private key in an environment variable and a script that signs locally. I understand why. It works and it is fast to set up. The problem is that anything with access to that process can read the key. And there is nothing between the script's logic and the signature, so a bug in the strategy becomes a signed transaction.
The four places a key can live
There are four common designs. They differ in what stands between a compromised backend and a signature.
The first is a cloud KMS like AWS KMS. The key stays in the HSM and your backend asks it to sign a hash. Nothing stands between your backend and a signature except IAM permissions. The KMS does not know what a blockchain transaction is.
The second is a hosted signer with a policy engine. The provider holds the key and checks your allowlists and caps before signing. The rule can see the transaction fields. It cannot see your ledger, a price, or whether a deposit landed on another chain.
The third is a rule written as code that runs inside the enclave with the key. The rule can fetch whatever it needs. This is how Lit works.
The fourth is a smart account with modules. A Safe or an ERC-4337 account enforces the rule on-chain. This is fully public, but it costs gas per operation, it only works on EVM chains, and the rule can only use data that is already on-chain.
Writing the rule on Lit
This Lit Action signs a settlement only after it confirms the deposit on the source chain.
// settle-v1.js
async function main({ pkpId, orderId, to, amount }) {
const src = await fetch(`https://indexer.example.com/deposits/${orderId}`).then(r => r.json());
if (!src.confirmed || src.amount !== amount) return { signed: false, reason: "deposit not confirmed" };
const wallet = new ethers.Wallet(await Lit.Actions.getPrivateKey({ pkpId }));
return { signed: true, tx: await wallet.signTransaction({ to, value: amount, chainId: 8453 }) };
}Your backend calls it like this, using the execute-only key.
jq -n --rawfile code settle-v1.js '{code: $code, js_params: {pkpId: "<pkp>", orderId: "o-123", to: "0x...", amount: "5000000"}}' \
| curl -s -X POST https://api.chipotle.litprotocol.com/core/v1/lit_action \
-H "X-Api-Key: $USAGE_KEY" -H "Content-Type: application/json" -d @-When the request comes in, the enclave hashes the code and checks that your API key is allowed to run that hash against that wallet. Then it runs the function and returns the signed transaction. The key never leaves the enclave. We cannot read it either.
What the execute-only key cannot do
It cannot create a wallet, add a rule, or change a group. Those actions need your account key. If you use ChainSecured mode, they need a transaction from the wallet that owns your account on Base. Keep both of those away from your production servers.
Some advice that applies no matter what you use
Give each service its own execute-only key, scoped to one group. Put the amount cap and the destination check in the signer's rule, not just in your own code. Test revoking a key before you launch, and notice that the wallet address does not change when you do. Keep the wallet's balance at a working level. A rule stops signatures that break the rule. It cannot undo a mistake that the rule allowed.
Get started
To create an account, go to the Lit dashboard and click New to Lit? Create an account. The API reference is at developer.litprotocol.com/management/api_direct.
This is part 3 of a five-part series on building with Lit. Part 4 comes out on Monday, October 5, and is about adding wallets to your app with an API.