Skip to main content

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.

Deadman Vault’s entire on-chain mechanics are built on top of FlowVault — a programmable token-routing smart contract on Stacks. Rather than writing a custom Clarity contract for every routing concern, Deadman Vault composes FlowVault’s three primitives — LOCK, SPLIT, and HOLD — to enforce heartbeat deadlines, extend them automatically on check-in, and route USDCx to beneficiaries when a vault flatlines. The keeper (a server-held Stacks keypair) is the entity that signs all FlowVault calls; the vault owner’s wallet only ever signs the initial USDCx transfer to the keeper address.

What is FlowVault?

FlowVault (flowvault-v2) is a smart contract on Stacks that provides programmable token routing via three composable primitives. Vault state is keyed off tx-sender — only the address that deposited can withdraw or modify routing rules. This makes the FlowVault SDK a natural fit for a custody layer, and it’s the reason Deadman Vault needs the custodial keeper pattern: a server-held key that can act as tx-sender on the owner’s behalf once they can no longer sign.
FlowVault SDK documentation is available at https://docs.flow-vault.dev/.

Primitives Used

LOCK — Heartbeat Period Enforcement

The lockAmount + lockUntilBlock combination is the core of Deadman Vault’s proof-of-life mechanism. Funds deposited under a LOCK rule cannot be withdrawn by the keeper until lockUntilBlock is reached. This creates the heartbeat window: if the keeper tries to withdraw early (e.g., during an accidental trigger), FlowVault rejects it.
  • lockAmount: the micro-USDCx amount placed under the time lock
  • lockUntilBlock: the Stacks block height at which the lock expires (calculated as currentBlock + heartbeatIntervalBlocks)

SPLIT — Beneficiary Distribution

The splitAddress + splitAmount routing rule directs a specific amount to a beneficiary address at payout time. When a vault triggers and the keeper calls setRoutingRules with a SPLIT configuration before withdraw, FlowVault routes the designated amount to the split address automatically.
  • splitAddress: the beneficiary’s Stacks address
  • splitAmount: the micro-USDCx amount allocated to that beneficiary

HOLD — Active Custody (Not Used in Deadman Flow)

HOLD represents an unlocked balance available for immediate owner withdrawal. Deadman Vault does not use HOLD during the active lifecycle — all deposited funds are placed under LOCK. HOLD becomes relevant only after the lock expires and the keeper has withdrawn the balance prior to distributing payouts.

Lock Stacking — Heartbeat Extension

Lock stacking is FlowVault’s mechanism for extending an existing lock without losing the accumulated balance. Each time the owner checks in (heartbeat), the keeper calls setRoutingRules with a new lockUntilBlock, then calls deposit with the minimum heartbeat amount (0.01 USDCx). FlowVault extends the lock on the entire existing balance to the new deadline. No funds are lost; the lock period simply rolls forward.
Lock stacking is what makes the heartbeat economical. The owner doesn’t re-deposit their full vault balance on every check-in — they deposit a nominal 0.01 USDCx and the entire existing locked balance gets its deadline extended.

Contract Addresses (Testnet)

ContractAddress
FlowVault (flowvault-v2)STD7QG84VQQ0C35SZM2EYTHZV4M8FQ0R7YNSQWPD.flowvault-v2
USDCx tokenST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx
These values are sourced from environment variables:
NEXT_PUBLIC_FLOWVAULT_CONTRACT_ADDRESS=STD7QG84VQQ0C35SZM2EYTHZV4M8FQ0R7YNSQWPD
NEXT_PUBLIC_FLOWVAULT_CONTRACT_NAME=flowvault-v2
NEXT_PUBLIC_FLOWVAULT_TOKEN_CONTRACT_ADDRESS=ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
NEXT_PUBLIC_FLOWVAULT_TOKEN_CONTRACT_NAME=usdcx

TypeScript Types

The FlowVault SDK exposes two key types used throughout the keeper layer:
interface RoutingRules {
  lockAmount: bigint;
  lockUntilBlock: number;
  splitAddress: string | null;
  splitAmount: bigint;
}

interface VaultState {
  totalBalance: bigint;
  lockedBalance: bigint;
  unlockedBalance: bigint;
  lockUntilBlock: number;
  currentBlock: number;
  routingRules: RoutingRules | null;
}
VaultState is fetched server-side via flowVault.getVaultState(keeperAddress) in the keeper cron and settlement logic. unlockedBalance determines whether the keeper needs to call withdraw before distributing payouts — if it’s already zero (e.g., lock hasn’t expired), settlement waits.

Vault Creation Flow

1

Owner deposits USDCx to keeper

The owner signs a SIP-010 USDCx transfer from their wallet to the vault’s keeper address using signDepositTransfer() in src/lib/flowvault-client.ts. This is the only transaction the owner ever signs.
2

Keeper calls setRoutingRules with LOCK

The keeper calls flowVault.setRoutingRules({ lockAmount, lockUntilBlock }) using the FlowVault SDK. At this stage, no SPLIT rules are set — the vault is in active custody mode with a single time lock.
3

Keeper calls deposit

The keeper calls flowVault.deposit(amount) to lock the received USDCx balance in FlowVault. The funds are now locked until lockUntilBlock and cannot be withdrawn by anyone, including the keeper, until the block height is reached.
4

Vault status set to active

The API route updates the vault’s Supabase record to status: "active" with the confirmed tx_id, last_heartbeat_block, and heartbeat_deadline_block.

Heartbeat Flow

1

Owner sends 0.01 USDCx to keeper

The owner signs a USDCx transfer of exactly 0.01 USDCx to the keeper address. The keeper cron detects the confirmed transaction and begins the heartbeat update.
2

Keeper calls setRoutingRules with new lockUntilBlock

The keeper calls setRoutingRules with an updated lockUntilBlock calculated from the current block height plus the vault’s configured heartbeat interval in blocks. This updates the routing rules in FlowVault without touching the locked balance yet.
3

Keeper calls deposit — lock stacking activates

The keeper calls flowVault.deposit(HEARTBEAT_AMOUNT) with the 0.01 USDCx received from the owner. FlowVault’s lock-stacking mechanism extends the deadline on the entire existing locked balance to the new lockUntilBlock. The vault stays continuously locked without the owner re-depositing their full balance.
4

Vault deadline updated in Supabase

updateVaultDeadline() in src/lib/supabase/queries.ts records the new last_heartbeat_block and heartbeat_deadline_block and resets the vault status to "active".

Settlement Flow (Triggered Vault)

When the keeper cron determines that a vault has missed its deadline and the grace period has expired, settleVaultPayout() in src/lib/keeper/settlement.ts handles the on-chain payout:
1

Keeper fetches vault state

flowVault.getVaultState(keeperAddress) is called. unlockedBalance is checked — if greater than zero, the lock has expired and withdrawal is possible.
2

Keeper calls withdraw

if (unlockedBalance > BigInt(0)) {
  await flowVault.withdraw(unlockedBalance);
}
The full unlocked balance is withdrawn from FlowVault to the keeper’s own address. After this point, the funds are held by the keeper account rather than locked in FlowVault.
3

Keeper distributes shares to beneficiaries

For each unpaid beneficiary whose vesting_release_at has passed, the keeper calls sendTokenTransfer() with the beneficiary’s proportional share (calculated from total_deposited and allocation_percent). Allocation is computed proportionally across all beneficiaries (not just unpaid ones) so rounding is consistent across partial retries.
4

Claims recorded and vault finalized

Each successful transfer calls recordClaim() to mark the beneficiary as paid in Supabase. Once all beneficiaries are claimed, updateVaultStatus(vault.id, "claimed") moves the vault to its terminal state.
Settlement is idempotent — it is safe to call repeatedly. The keeper cron runs it on every sweep for triggered vaults, and the beneficiary’s “Check & Release Funds” button triggers it on demand. Beneficiaries whose transfers failed will be retried on the next call.

The tx-sender Constraint

FlowVault’s key constraint shapes every architectural decision in Deadman Vault:
Vault state is keyed off tx-sender with no owner/vault-id argument. Only the address that called deposit can ever call withdraw.
This is correct behavior for a standard DeFi vault — but it means a dead owner cannot authorize a withdrawal by definition. There are only two ways to handle this:
  1. Custom Clarity contract (the non-custodial production fix): a wrapper contract with a permissionless claim function that enforces the heartbeat deadline on-chain. No server holds keys.
  2. Custodial keeper (the testnet approach): a server-held keypair is tx-sender. It can act on behalf of the vault after the owner is gone — but it is a trusted third party.
Deadman Vault uses option 2 on testnet. See Security Model for the full implications and the production-grade path forward.

Build docs developers (and LLMs) love