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 sends automated email notifications at every critical lifecycle transition — from the first warning that a vault is nearing its deadline, through the grace period countdown, all the way to per-beneficiary payout confirmations with Stacks explorer links. The email system is built on Resend and is designed to fail gracefully: if RESEND_API_KEY or RESEND_FROM_EMAIL are missing, all notification functions silently no-op and vault operations proceed normally. Emails are informational — funds are transferred regardless of whether email delivery succeeds.

Setup

1

Create a Resend account

Sign up at resend.com and generate an API key from your dashboard.
2

Verify a sender domain

In your Resend dashboard, add and verify the domain you’ll send from. RESEND_FROM_EMAIL must use a verified domain — for example, noreply@yourdomain.com. Resend will reject sends from unverified domains.
3

Set environment variables

RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@yourdomain.com
If RESEND_FROM_EMAIL is set to an address on an unverified domain, Resend will throw an error. The sendEmail() function catches this and logs it, so vault operations won’t break — but emails won’t be delivered.

Low-Level Primitive: sendEmail()

All notification functions call sendEmail() internally. It is the only function that directly invokes the Resend SDK:
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendEmail({
  to,
  subject,
  html,
}: {
  to: string;
  subject: string;
  html: string;
}) {
  if (!process.env.RESEND_API_KEY || !process.env.RESEND_FROM_EMAIL) {
    console.warn("RESEND not configured, skipping email");
    return;
  }

  try {
    await resend.emails.send({
      from: process.env.RESEND_FROM_EMAIL,
      to,
      subject,
      html,
    });
  } catch (error) {
    console.error("Failed to send email:", error);
  }
}
The guard at the top means the entire notifications subsystem is opt-in. Vaults work end-to-end without any email configuration.

Deduplication

Warning and grace-period emails are deduplicated to prevent spamming the owner during the keeper cron’s frequent sweeps. The deduplication mechanism lives in src/lib/supabase/queries.ts:
export async function hasRecentNotification(
  vaultId: string,
  type: string,
  hours: number
): Promise<boolean> {
  const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
  const { count, error } = await supabase
    .from("notifications")
    .select("*", { count: "exact", head: true })
    .eq("vault_id", vaultId)
    .eq("type", type)
    .gte("sent_at", since);

  if (error) throw new Error(error.message);
  return (count || 0) > 0;
}
Before sending a warning or grace-period email, the keeper cron calls hasRecentNotification(vaultId, type, hours). If a notification of the same type was already sent for that vault within the dedup window, the send is skipped. Each sent notification is recorded in the notifications table via addNotification().

Notification Functions

sendWarningEmail(vault, ownerEmail)

Sent to the vault owner when the vault status transitions to "warning" — triggered when the remaining blocks until deadline fall to 432 blocks or fewer (approximately 3 days on Stacks mainnet cadence).
export async function sendWarningEmail(vault: any, ownerEmail: string) {
  if (!ownerEmail) return;

  await sendEmail({
    to: ownerEmail,
    subject: "⚠️ Deadman Vault — Check In Before It's Too Late",
    html: `...`,
  });
}
  • Recipient: vault owner
  • Subject: ⚠️ Deadman Vault — Check In Before It's Too Late
  • Trigger condition: deadlineBlock - currentBlock <= 432 (≈ 3 days)
  • Deduplication: once per 24 hours per vault
  • Body: includes vault name and a direct link to the heartbeat check-in page

sendGracePeriodEmail(vault, ownerEmail)

Sent to the vault owner when the vault enters "grace_period" status — the 48-hour window after the heartbeat deadline passes but before the vault fully triggers.
export async function sendGracePeriodEmail(vault: any, ownerEmail: string) {
  if (!ownerEmail) return;

  await sendEmail({
    to: ownerEmail,
    subject: "🚨 Deadman Vault — 48 Hour Grace Period",
    html: `...`,
  });
}
  • Recipient: vault owner
  • Subject: 🚨 Deadman Vault — 48 Hour Grace Period
  • Trigger condition: vault status is "grace_period"
  • Deduplication: once per 48 hours per vault
  • Body: includes vault name and an urgent check-in link

sendClaimReadyEmail(beneficiary, vault)

Sent to each beneficiary the moment a vault flatlines and triggers. This is a heads-up notification — it tells the beneficiary they’ve been named and what to expect. If vesting is configured (vesting_days > 0), the email shows the vesting_release_at date and explains that funds will be sent automatically at that time. Silently no-ops if the beneficiary has no email field.
export async function sendClaimReadyEmail(beneficiary: any, vault: any) {
  if (!beneficiary.email) return;

  const releaseAt = beneficiary.vesting_release_at
    ? new Date(beneficiary.vesting_release_at)
    : null;
  const isDelayed = releaseAt && releaseAt.getTime() > Date.now();

  await sendEmail({
    to: beneficiary.email,
    subject: "💰 Deadman Vault — You've Been Named a Beneficiary",
    html: `...`,
  });
}
  • Recipient: each beneficiary (individually)
  • Subject: 💰 Deadman Vault — You've Been Named a Beneficiary
  • Trigger condition: vault status transitions to "triggered"
  • Deduplication: none (sent exactly once per beneficiary per trigger)
  • Vesting awareness: if vesting_release_at is in the future, the email shows the release date; otherwise it confirms funds are being sent immediately
  • No email, no problem: beneficiaries without an email field still receive their funds — the on-chain transfer happens regardless

sendFundsReleasedEmail(beneficiary, vault, amount, txId)

Sent to a beneficiary after their token transfer has been confirmed on-chain. Includes the exact amount transferred, the beneficiary’s wallet address, and a direct link to the transaction on the Stacks testnet explorer.
export async function sendFundsReleasedEmail(
  beneficiary: any,
  vault: any,
  amount: string,
  txId: string
) {
  if (!beneficiary.email) return;

  const explorerUrl = `https://explorer.stacks.co/txid/${txId}?chain=testnet`;

  await sendEmail({
    to: beneficiary.email,
    subject: "✅ Deadman Vault — Funds Sent to Your Wallet",
    html: `...`,
  });
}
  • Recipient: each beneficiary (individually, after their specific transfer confirms)
  • Subject: ✅ Deadman Vault — Funds Sent to Your Wallet
  • Trigger condition: sendTokenTransfer() succeeds in settleVaultPayout()
  • Deduplication: none (called exactly once per successful transfer)
  • Explorer link: https://explorer.stacks.co/txid/{txId}?chain=testnet
  • Amount: passed as a human-readable token string (e.g., "12.50") via microToToken() from the FlowVault SDK

Notification Events Reference

EventRecipientTrigger ConditionDedup Window
sendWarningEmailVault ownerdeadlineBlock - currentBlock <= 43224 hours
sendGracePeriodEmailVault ownerVault enters grace_period status48 hours
sendClaimReadyEmailEach beneficiaryVault transitions to triggeredNone (once per trigger)
sendFundsReleasedEmailEach beneficiaryOn-chain transfer confirmed in settlementNone (once per transfer)

Notifications Table Schema

The notifications table records every sent email and is used exclusively for deduplication lookups:
ColumnTypeDescription
iduuidPrimary key
vault_iduuidForeign key to the vault
typetextNotification type key (e.g., "warning", "grace_period")
recipient_emailtextEmail address the notification was sent to (optional)
recipient_addresstextStacks address of the recipient (optional)
statustextDelivery status of the notification
sent_attimestamptzTimestamp of when the notification was recorded
The notifications table is protected by the same RLS policy as all other tables — zero public access, service-role key only. Deduplication queries run server-side in the keeper cron.

Build docs developers (and LLMs) love