The keeper is the autonomous enforcement layer of Deadman Vault. Without it, a missed heartbeat is just a database record — with it, a missed heartbeat becomes an irreversible on-chain event that settles funds directly to beneficiaries. Running as a Vercel Cron Job, the keeper continuously sweeps all live vaults, evaluates their deadline status against the current Stacks block height, sends escalating notifications to owners, and — once the grace period expires — locks in the trigger, schedules vesting, and distributes USDCx proportionally to every eligible beneficiary. It is the difference between an intent and a guarantee.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/natureloved/DeadMan-Vault/llms.txt
Use this file to discover all available pages before exploring further.
How the Keeper is Invoked
The keeper runs as a Vercel Cron Job that callsGET /api/keeper. The handler fetches the current Stacks block height and passes it to runKeeper:
runKeeper returns a summary of every vault it evaluated and every action it took during that sweep.
What runKeeper Does
runKeeper(currentBlock) loads all vaults in sweepable states (active, warning, grace_period, triggered) via getVaultsForKeeperSweep, then evaluates each one in order. For each vault, it performs up to four distinct actions:
Retry settlement for already-triggered vaults
If a vault is already in
triggered status and has at least one unclaimed beneficiary (including those whose vesting cliff has matured since the last sweep), settleVaultPayout is called again. This handles transient RPC failures, network timeouts, and beneficiaries whose cliff window just opened.Send warning email when deadline is within 432 blocks
When
deadline − currentBlock ≤ WARNING_BLOCKS (432 blocks, ≈ 3 days) and the deadline has not yet passed, the keeper sends a warning email to the vault owner and advances status to warning. This notification is deduplicated — it fires at most once per 24-hour window per vault.Advance to grace_period when deadline passes
When
currentBlock > deadline and the grace window (currentBlock ≤ deadline + 288) is still open, the keeper advances status to grace_period and sends a critical grace period email. This notification is deduplicated to at most once per 48 hours.Trigger vault when grace period expires
When
currentBlock > deadline + GRACE_PERIOD_BLOCKS (288 blocks, ≈ 2 days after deadline), the vault is irreversibly triggered. The keeper:- Sets
statustotriggeredand recordstriggered_at - Calls
scheduleVestingto compute and persist each beneficiary’svesting_release_at - Re-fetches beneficiaries so settlement sees the freshly-computed vesting timestamps
- Calls
settleVaultPayoutto attempt immediate payout - Sends claim-ready emails to every beneficiary and logs each notification
The Custodial Keeper Keypair
Each vault has its own dedicated Stacks keypair, generated at vault creation time bygenerateKeeperAccount. The private key is encrypted with AES-256-GCM using a server-side KEEPER_ENCRYPTION_KEY environment variable and stored in the keeper_key_ciphertext column:
decrypt(vault.keeper_key_ciphertext) to recover the private key, instantiates a FlowVault client with that key, and submits transactions on behalf of the vault owner.
How settleVaultPayout Works
Settlement is handled by settleVaultPayout in src/lib/keeper/settlement.ts. It is designed to be idempotent — safe to call multiple times without double-paying anyone:
Filter to unpaid, vesting-matured beneficiaries
Only beneficiaries where
claimed = false and either vesting_release_at is null or has already passed are included in the current payout run.Withdraw unlocked balance from FlowVault
The keeper checks the vault’s
unlockedBalance via flowVault.getVaultState(). If there is an unlocked balance greater than zero, it calls flowVault.withdraw(unlockedBalance) to move funds to the keeper’s wallet.Calculate each beneficiary's proportional share
Shares are computed in micro-USDCx from
total_deposited, using integer arithmetic to avoid rounding drift:Send USDCx to each eligible beneficiary
For each unpaid beneficiary,
sendTokenTransfer dispatches a USDCx transfer directly to their Stacks address. On success, recordClaim marks the beneficiary as claimed and stores the transaction ID.Return Type
runKeeper returns a structured summary for logging and monitoring. The return type is declared inline in the source:
checked— total number of vaults evaluated in this sweepactions— action tags emitted during the sweep, e.g.["warning:uuid", "triggered:uuid", "settle-retry:uuid"]
actions array uses colon-delimited tags (action:vaultId) so that each entry is both human-readable in logs and machine-parseable for alerting pipelines.
Notifications Sent by the Keeper
| Trigger | Function | Deduplication |
|---|---|---|
| Deadline within 432 blocks | sendWarningEmail | Once per 24 hours |
| Deadline passed, grace open | sendGracePeriodEmail | Once per 48 hours |
| Vault triggered | sendClaimReadyEmail (per beneficiary) | Not deduplicated |
| Beneficiary paid | sendFundsReleasedEmail | Not deduplicated |
notifications table with vault_id, type, recipient_email, recipient_address, and sent_at — providing a complete audit trail of every keeper communication.