ledgerkeep-cli is the off-chain keeper. It reads a contract's time-to-live over RPC — the only place the countdown is visible — reports how close entries are to expiry, and extends the ones running low. It provides the lkeep command.
Requires Node.js 22 or 24.
git clone https://github.com/ledgerkeep/ledgerkeep-cli
cd ledgerkeep-cli
npm install
npm run build
npm linkCopy .env.example to .env and fill it in.
| Variable | Purpose |
|---|---|
LK_RPC_URL |
Soroban RPC endpoint |
LK_NETWORK_PASSPHRASE |
Network passphrase |
LK_REGISTRY_ID |
Deployed registry contract ID |
LK_KEEPER_KEY |
Path to the keeper's signing key file |
LK_THRESHOLD |
Extend when remaining ledgers fall below this |
LK_EXTEND_TO |
Target time-to-live in ledgers after extension |
LK_SCAN_INTERVAL_MS |
Daemon scan period |
LK_KEEPER_KEY is a path to a key file, never a secret passed on the command line. The signing key is read only at the moment of signing and is never logged. Read-only commands ignore it entirely — you can scan and list without a key present.
The tool extends time-to-live two ways, and the difference is not cosmetic.
Path A — call the contract's extend_all. For a contract that adopts the standard, the CLI invokes its permissionless extend_all. The contract extends its own declared keys and records the maintenance on-chain, which is what makes a keeper eligible to claim a tip. This is the default and the only path that earns a tip.
Path B — raw extendFootprintTtl. For a contract that has not adopted the standard, the CLI extends its ledger keys directly. This works on any contract, records no maintenance, and earns no tip. It exists so the tool is useful against contracts that predate the standard.
The tool never silently switches between them. Path A is the default; Path B is an explicit flag. They mean different things and the operator chooses.
Reads the time-to-live of a contract's instance entry and, if the contract is registered, each key in its manifest. Prints a table: each key, its remaining ledgers, and a status of ok, low, or archived. Signs nothing, needs no key. Exits non-zero if any key is low or archived, so it works as a shell check.
Extends one contract. Defaults to Path A — invoking extend_all. The --footprint flag forces Path B against the instance key and any keys you name. Simulates first, prints the fee, and prints the transaction hash on success. Needs a funded keeper key.
Pages through the registry and prints every registered contract with its declared key count and terms. Read-only.
The daemon. On each scan interval it discovers every registered contract, scans each, and extends the ones that have fallen below their threshold — via Path A. It reports drift, survives a single contract's failure, and continues. See Running a keeper for how to run it safely, including an important limitation about maintenance cost.
The public testnet RPC occasionally returns "account not found" for a live, funded account. A one-shot lkeep extend will exit non-zero if it hits this — retry it. The daemon survives it: it logs, continues, and resumes on the next tick. For anything you rely on, use a dedicated RPC endpoint rather than the public one.