← All articles

How to run a scheduled job that signs blockchain transactions without running a server

Put the job's logic in a Lit Action, run it on a schedule with Lit Triggers, and sign with a wallet whose key stays in a secure enclave. Here is a job that keeps your hot wallets topped up with gas, with limits written into its code.

Lit Protocol

To run a scheduled job that signs blockchain transactions without running a server, write the job as a Lit Action and let Lit Triggers run it on a schedule. A Lit Action is a JavaScript function that runs inside a secure enclave. Lit Triggers calls it on a cron schedule, as often as every 30 seconds. The action signs with a Lit wallet whose key stays in the enclave, and the rules for what it can send are written into its code. You do not run a server, because Lit runs the Triggers service that calls your job, and the private key never leaves the enclave.

As an example, I will build a job that checks a list of hot wallets every 15 minutes and tops up any wallet whose ETH balance on Base has dropped below a floor.

The job

// gas-topup.js
const PKP = "0xYourFundingWallet...";           // the Lit wallet that pays
const WALLETS = ["0xHotWalletOne...", "0xHotWalletTwo..."];
const FLOOR = ethers.utils.parseEther("0.02");  // top up below this
const TARGET = ethers.utils.parseEther("0.05"); // fill up to this
const MAX_PER_RUN = ethers.utils.parseEther("0.1");

const main = async () => {
  const provider = new ethers.providers.StaticJsonRpcProvider("https://mainnet.base.org");
  const funder = new ethers.Wallet(await Lit.Actions.getPrivateKey({ pkpId: PKP }), provider);
  let spent = ethers.BigNumber.from(0);
  const sent = [];
  for (const w of WALLETS) {
    const balance = await provider.getBalance(w);
    if (balance.gte(FLOOR)) continue;
    const amount = TARGET.sub(balance);
    if (spent.add(amount).gt(MAX_PER_RUN)) break;
    const tx = await funder.sendTransaction({ to: w, value: amount });
    await tx.wait();
    spent = spent.add(amount);
    sent.push({ to: w, amount: ethers.utils.formatEther(amount), hash: tx.hash });
  }
  return { sent };
};

The wallets, the floor, the target, and the limit per run are all constants in the code. The job only fills a wallet up to the target, so if it runs more often than you planned, one run after another, it does not send more than the wallets need. It waits for each transfer to confirm, so the nonces stay in order.

Set up the wallet and the key

Create an account at the Lit dashboard by clicking Create an account. Create a Lit wallet, put it and this action in a group, and make a usage key that can only run that group. The post on adding wallets to your app with an API shows each call. Then send some ETH on Base to the funding wallet.

The job signs with a Lit wallet that belongs to your account. Lit Actions also have their own key, but anyone who runs the same code gets that same key, so a job that moves money should use a wallet. Only usage keys from your account can use your wallet.

Put it on a schedule

Lit Triggers runs an action on a schedule, when a webhook arrives, or when an event happens on-chain. Authorize it once by following the pairing steps in the Lit Triggers docs. You create a token on your machine and approve it in your browser. Then create a schedule trigger.

jq -n --rawfile code gas-topup.js --arg key "$USAGE_KEY" \
  '{name: "gas-topup", kind: "schedule", action_code: $code, usage_api_key: $key, config: {cron: "*/15 * * * *"}}' \
  | curl -fsS -X POST https://triggers.litprotocol.com/api/triggers \
      -H "authorization: Bearer $TOKEN" -H "content-type: application/json" -d @-

Every run is recorded with its status and what the job returned, so you can see each top-up it sent.

curl -fsS -H "authorization: Bearer $TOKEN" "https://triggers.litprotocol.com/api/triggers/<id>/runs?limit=20"

If Lit returns a server error, including when the job throws, Triggers tries the run up to three times. To pause the job, set enabled to false on the trigger.

What the setup protects

The funding key never leaves the enclave. The Triggers service, which Lit runs, stores your usage key encrypted, and that key can only run this job with this wallet. Someone who steals it can run the job early or several times at once. The job only tops up wallets that are below the floor, up to the limit per run, so overlapping runs can overfill a wallet, but the funds only go to your own hot wallets. Keep the funding wallet's balance at what you are comfortable spending.

Get started

To create an account, go to the Lit dashboard and click Create an account. Lit Triggers is documented at developer.litprotocol.com/lit-triggers.