To understand what LedgerKeep does, you need three facts about how Soroban stores data. None of them are obscure, but together they create a problem that has no built-in solution.
Soroban does not store data forever by default. Every ledger entry — a contract's instance, its compiled code, and each piece of data it writes — carries a time-to-live, measured in ledgers. Ledgers close every few seconds, so a time-to-live is effectively a countdown in real time.
When the countdown reaches zero, the entry is archived. It leaves the active state that contracts read and write, and stops taking up space the network charges for. This is deliberate: it keeps the ledger from growing without bound as old, unused data accumulates.
There are two storage durabilities that matter here:
- Persistent — balances, records, anything that must survive. When archived, it can be restored.
- Instance — a contract's own configuration and small global state, sharing the lifetime of the contract instance itself.
There is a third, temporary, which is deleted permanently when it expires and cannot be restored. LedgerKeep does not use it.
Before Protocol 23, an archived persistent entry was a serious problem to recover. Since Protocol 23, recovery is automatic: when a transaction touches an archived persistent entry, it is restored as part of that transaction, mostly transparently. The main visible effect is a higher fee for that transaction.
So the honest framing is not "your data is lost forever." It is:
- Restoring costs more than keeping an entry alive would have.
- While a contract's instance or code entry is archived, the contract is unusable until restored — you cannot call a contract whose code has been archived without restoring it first.
- You have no warning. Nothing tells you an entry is about to expire.
That last point is the one LedgerKeep exists for.
This is the fact that shapes everything.
A Soroban contract, while it runs, cannot read the time-to-live of its own entries. The function that would tell it — get_ttl — exists only in the test environment, not on-chain. A live contract has no way to ask "how many ledgers until my balance entry expires?"
It can extend time-to-live. The host provides extend_ttl, which pushes an entry's countdown further out. But extension is conditional: it only does anything when the remaining time-to-live has already fallen below a threshold you specify. And the contract still cannot see the current value — it can only ask the host to extend, and the host decides whether extension is needed.
So a contract can keep its own data alive, but it cannot know when to. It is like being able to top up a battery you cannot check the level of.
Put the three facts together:
- Entries expire on a countdown.
- Expiry costs money and can make a contract unusable.
- A contract cannot see its own countdown.
The observation — knowing when an entry is close to expiry — has to come from outside the contract. Off-chain, over RPC, you can read an entry's time-to-live and compare it against the current ledger. That is the only place the countdown is visible.
LedgerKeep splits the problem along exactly this line. The contract does what only it can do: extend its own keys and record that it did. The off-chain keeper does what only it can do: watch the countdown and decide when extension is needed. The architecture follows directly from that division.