From 559b58d127197ca1980c360ec0e7ffbf4ca12439 Mon Sep 17 00:00:00 2001 From: FedericoMusa <165095964+FedericoMusa@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:06:57 -0300 Subject: [PATCH] feat: localized emails (es, pt, de, fr, ja) --- app/emails.py | 592 ++++++++++++++++++++++++++++++++++++++++++++++++++ app/main.py | 264 ++++------------------ app/models.py | 2 + 3 files changed, 631 insertions(+), 227 deletions(-) create mode 100644 app/emails.py diff --git a/app/emails.py b/app/emails.py new file mode 100644 index 0000000..a43e951 --- /dev/null +++ b/app/emails.py @@ -0,0 +1,592 @@ +# SPDX-License-Identifier: BUSL-1.1 +# Copyright (c) 2026 pyoneerC. All rights reserved. + +""" +Email templates for Deadhand Protocol. +Supports: English (en), Spanish (es), Portuguese (pt), German (de), French (fr), Japanese (ja) +""" + +SUPPORTED_LANGUAGES = ["en", "es", "pt", "de", "fr", "ja"] +DEFAULT_LANGUAGE = "en" + + +def get_language_from_email(email: str) -> str: + """ + Heuristic: detect language from email TLD. + Falls back to English if unknown. + """ + if not email or "@" not in email: + return DEFAULT_LANGUAGE + + domain = email.split("@")[-1].lower() + + if domain.endswith(".ar") or domain.endswith(".mx") or domain.endswith(".cl") or domain.endswith(".co") or domain.endswith(".pe") or domain.endswith(".uy"): + return "es" + if domain.endswith(".br"): + return "pt" + if domain.endswith(".de") or domain.endswith(".at") or domain.endswith(".ch"): + return "de" + if domain.endswith(".fr"): + return "fr" + if domain.endswith(".jp"): + return "ja" + + return DEFAULT_LANGUAGE + + +# --------------------------------------------------------------------------- +# SUBJECTS +# --------------------------------------------------------------------------- + +SUBJECTS = { + "welcome": { + "en": "not just a welcome email (and a drawing for you)", + "es": "no es solo un email de bienvenida (y un dibujo para vos)", + "pt": "não é só um e-mail de boas-vindas (e um desenho pra você)", + "de": "nicht nur eine Willkommens-E-Mail (und eine Zeichnung für dich)", + "fr": "pas juste un e-mail de bienvenue (et un dessin pour vous)", + "ja": "ただの歓迎メールじゃありません(あなたへの絵も添えて)", + }, + "cancellation": { + "en": "your deadhand vault has been deactivated", + "es": "tu vault de deadhand fue desactivado", + "pt": "seu vault do deadhand foi desativado", + "de": "dein deadhand-Vault wurde deaktiviert", + "fr": "votre coffre deadhand a été désactivé", + "ja": "deadhandのvaultが無効化されました", + }, + "reminder_30d": { + "en": "quick check-in from Deadhand", + "es": "aviso rápido de Deadhand", + "pt": "lembrete rápido do Deadhand", + "de": "kurze Erinnerung von Deadhand", + "fr": "petit rappel de Deadhand", + "ja": "Deadhandからの確認", + }, + "warning_60d": { + "en": "urgent: we haven't heard from you in 60 days", + "es": "urgente: no sabemos nada de vos hace 60 días", + "pt": "urgente: não temos notícias suas há 60 dias", + "de": "dringend: wir haben seit 60 Tagen nichts von dir gehört", + "fr": "urgent : sans nouvelles depuis 60 jours", + "ja": "緊急:60日間連絡がありません", + }, + "death": { + "en": "important: digital recovery key for {owner_email}", + "es": "importante: clave de recuperación digital de {owner_email}", + "pt": "importante: chave de recuperação digital de {owner_email}", + "de": "wichtig: digitaler Wiederherstellungsschlüssel für {owner_email}", + "fr": "important : clé de récupération numérique de {owner_email}", + "ja": "重要:{owner_email}のデジタル回復キー", + }, +} + + +# --------------------------------------------------------------------------- +# EMAIL BODIES +# --------------------------------------------------------------------------- + +BASE_STYLES = """ + body { font-family: Georgia, serif; line-height: 1.6; color: #222; + max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #fff; } + h1 { font-size: 22px; color: #000; font-weight: normal; margin-top: 0; } + .heartbeat-link { display: inline-block; color: #000 !important; + text-decoration: underline; font-weight: bold; margin: 20px 0; } + .footer { font-size: 11px; color: #999; margin-top: 60px; + border-top: 1px solid #eee; padding-top: 20px; } + .warning-box { border: 1px dashed #ff4444; padding: 20px; margin: 20px 0; } + .shard-box { background: #fefefe; border: 1px dashed #ccc; padding: 25px; + margin: 30px 0; font-family: monospace; font-size: 13px; + word-break: break-all; color: #222; } + .instructions { background: #fff; border: 1px solid #eee; padding: 20px; + border-radius: 4px; margin: 30px 0; } + .cta-box { background: #fafafa; border: 1px solid #ddd; padding: 25px; + margin-top: 40px; text-align: center; } + .cta-link { display: inline-block; background: #222; color: #fff !important; + text-decoration: none; padding: 12px 20px; border-radius: 4px; + font-weight: bold; margin-top: 15px; } +""" + + +def _html_wrap(body: str) -> str: + return f""" + + +{body} +""" + + +# --------------------------------------------------------------------------- +# WELCOME EMAIL +# --------------------------------------------------------------------------- + +def get_welcome_email(lang: str, email: str, beneficiary_email: str, heartbeat_link: str) -> str: + bodies = { + "en": f""" +

it's not just a welcome email.

+

hey,

+

you just did something most people never do: you protected your crypto for the people you love.

+

your vault is now active. here's what happens next:

+ +

vault active for: {email}
+ beneficiary: {beneficiary_email}

+

your first heartbeat link: click here to confirm you're alive

+

if you have any questions, just reply to this email. i read them all.

+

with care,
max
(the guy who sends you crayon drawings)

+ + """, + "es": f""" +

no es solo un email de bienvenida.

+

hola,

+

acabás de hacer algo que la mayoría de la gente nunca hace: protegiste tus cripto para las personas que querés.

+

tu vault ya está activo. esto es lo que pasa ahora:

+ +

vault activo para: {email}
+ beneficiario: {beneficiary_email}

+

tu primer link de heartbeat: hacé clic para confirmar que estás vivo

+

si tenés alguna pregunta, respondé este email. los leo todos.

+

con cuidado,
max
(el que te manda los dibujos con crayones)

+ + """, + "pt": f""" +

não é só um e-mail de boas-vindas.

+

olá,

+

você acabou de fazer algo que a maioria das pessoas nunca faz: protegeu suas criptos para as pessoas que ama.

+

seu vault já está ativo. veja o que acontece agora:

+ +

vault ativo para: {email}
+ beneficiário: {beneficiary_email}

+

seu primeiro link: clique aqui para confirmar que está vivo

+

se tiver dúvidas, responda este e-mail. eu leio todos.

+

com cuidado,
max

+ + """, + "de": f""" +

das ist nicht nur eine Willkommens-E-Mail.

+

hey,

+

du hast gerade etwas getan, das die meisten Menschen nie tun: du hast deine Krypto für deine Lieben gesichert.

+

dein Vault ist jetzt aktiv. so geht es weiter:

+ +

vault aktiv für: {email}
+ begünstigte(r): {beneficiary_email}

+

dein erster Heartbeat-Link: hier klicken um zu bestätigen, dass du am Leben bist

+

bei Fragen einfach auf diese E-Mail antworten.

+

mit Sorgfalt,
max

+ + """, + "fr": f""" +

ce n'est pas juste un e-mail de bienvenue.

+

bonjour,

+

vous venez de faire quelque chose que la plupart des gens ne font jamais : protéger vos cryptos pour les personnes que vous aimez.

+

votre vault est maintenant actif. voici ce qui se passe :

+ +

vault actif pour : {email}
+ bénéficiaire : {beneficiary_email}

+

votre premier lien heartbeat : cliquez ici pour confirmer que vous êtes en vie

+

pour toute question, répondez à cet e-mail. je les lis tous.

+

avec soin,
max

+ + """, + "ja": f""" +

これはただの歓迎メールではありません。

+

こんにちは、

+

あなたは多くの人がしないことをしました:愛する人のために暗号資産を守ったのです。

+

vaultが有効になりました。これからの流れ:

+ +

vault有効:{email}
+ 受取人:{beneficiary_email}

+

最初のハートビートリンク:生存確認のためここをクリック

+

ご質問はこのメールに返信してください。すべて読んでいます。

+

大切に、
max

+ + """, + } + return _html_wrap(bodies.get(lang, bodies["en"])) + + +# --------------------------------------------------------------------------- +# CANCELLATION EMAIL +# --------------------------------------------------------------------------- + +def get_cancellation_email(lang: str) -> str: + bodies = { + "en": """ +

hey,

+

your deadhand vault has been deactivated. your data has been deleted.

+

i'm not going to send you a survey or a discount code. i just want to say thanks for trusting deadhand for a while.

+

if you ever change your mind, the door is open.

+

take care,
max

+ + """, + "es": """ +

hola,

+

tu vault de deadhand fue desactivado. tus datos fueron eliminados.

+

no te voy a mandar una encuesta ni un cupón de descuento. solo quiero decirte gracias por haber confiado en deadhand.

+

si algún día cambiás de opinión, la puerta está abierta.

+

cuidate,
max

+ + """, + "pt": """ +

olá,

+

seu vault do deadhand foi desativado. seus dados foram deletados.

+

não vou te mandar pesquisa nem cupom de desconto. só quero dizer obrigado por ter confiado no deadhand.

+

se mudar de ideia, a porta está aberta.

+

cuide-se,
max

+ + """, + "de": """ +

hey,

+

dein deadhand-Vault wurde deaktiviert. deine Daten wurden gelöscht.

+

ich werde dir keine Umfrage oder einen Rabattcode schicken. ich möchte nur danke sagen, dass du deadhand vertraut hast.

+

wenn du deine Meinung änderst, ist die Tür offen.

+

pass auf dich auf,
max

+ + """, + "fr": """ +

bonjour,

+

votre vault deadhand a été désactivé. vos données ont été supprimées.

+

je ne vais pas vous envoyer un sondage ou un code de réduction. je veux juste vous remercier d'avoir fait confiance à deadhand.

+

si vous changez d'avis, la porte est ouverte.

+

prenez soin de vous,
max

+ + """, + "ja": """ +

こんにちは、

+

deadhandのvaultが無効化されました。データは削除されました。

+

アンケートや割引コードは送りません。deadhandを信頼してくれたことへの感謝だけ伝えたいです。

+

気が変わったら、いつでもどうぞ。

+

お気をつけて、
max

+ + """, + } + return _html_wrap(bodies.get(lang, bodies["en"])) + + +# --------------------------------------------------------------------------- +# 30-DAY REMINDER EMAIL +# --------------------------------------------------------------------------- + +def get_reminder_30d_email(lang: str, heartbeat_link: str) -> str: + bodies = { + "en": f""" +

hey,

+

just a quick check-in. your 30-day heartbeat timer is almost up.

+

click the link below to reset it — takes 5 seconds:

+ i'm here, reset the timer +

if you don't click it, no big deal for now. i'll check in again in another 30 days. but after 90 days of silence, we'll have to send shard c to your beneficiary.

+

stay safe,
deadhand protocol

+ + """, + "es": f""" +

hola,

+

solo un aviso rápido. tu timer de heartbeat de 30 días está por vencer.

+

hacé clic en el link para resetearlo — tarda 5 segundos:

+ estoy aquí, resetear el timer +

si no hacés clic, por ahora no pasa nada. te vuelvo a escribir en 30 días. pero después de 90 días de silencio, vamos a tener que enviar el shard c a tu beneficiario.

+

cuidate,
deadhand protocol

+ + """, + "pt": f""" +

olá,

+

só um lembrete rápido. seu timer de heartbeat de 30 dias está quase vencendo.

+

clique no link abaixo para resetá-lo — leva 5 segundos:

+ estou aqui, resetar o timer +

se não clicar, não tem problema por enquanto. vou te escrever novamente em 30 dias. mas após 90 dias de silêncio, precisaremos enviar o shard c para seu beneficiário.

+

cuide-se,
deadhand protocol

+ + """, + "de": f""" +

hey,

+

kurze Erinnerung. dein 30-Tage-Heartbeat-Timer läuft bald ab.

+

klick den Link unten, um ihn zurückzusetzen — dauert 5 Sekunden:

+ ich bin hier, Timer zurücksetzen +

wenn du nicht klickst, ist das erstmal kein Problem. ich melde mich in 30 Tagen wieder. aber nach 90 Tagen ohne Signal müssen wir Shard C an deinen Begünstigten senden.

+

bleib gesund,
deadhand protocol

+ + """, + "fr": f""" +

bonjour,

+

juste un petit rappel. votre timer heartbeat de 30 jours arrive bientôt à expiration.

+

cliquez sur le lien ci-dessous pour le réinitialiser — 5 secondes suffisent :

+ je suis là, réinitialiser le timer +

si vous ne cliquez pas, pas de panique pour l'instant. je vous recontacterai dans 30 jours. mais après 90 jours de silence, nous devrons envoyer le shard c à votre bénéficiaire.

+

prenez soin de vous,
deadhand protocol

+ + """, + "ja": f""" +

こんにちは、

+

簡単なお知らせです。30日間のハートビートタイマーがもうすぐ切れます。

+

下のリンクをクリックしてリセットしてください — 5秒で完了します:

+ ここにいます、タイマーをリセット +

クリックしなくても今はまだ大丈夫です。30日後に再度連絡します。ただし90日間応答がない場合、shard cを受取人に送る必要があります。

+

お気をつけて、
deadhand protocol

+ + """, + } + return _html_wrap(bodies.get(lang, bodies["en"])) + + +# --------------------------------------------------------------------------- +# 60-DAY WARNING EMAIL +# --------------------------------------------------------------------------- + +def get_warning_60d_email(lang: str, heartbeat_link: str) -> str: + bodies = { + "en": f""" +

hey,

+

i'm getting a little worried. we haven't heard from you in 60 days.

+
+

just 30 days left.

+

if you don't click the link below within the next month, our system will assume the worst and automatically send shard c to your beneficiary.

+
+

if you're just busy, i totally get it. but please, click this now:

+ i'm here, reset the timer +

talk soon,
deadhand protocol

+ + """, + "es": f""" +

hola,

+

estoy un poco preocupado. no sabemos nada de vos hace 60 días.

+
+

solo quedan 30 días.

+

si no hacés clic en el link en el próximo mes, el sistema va a asumir lo peor y enviará automáticamente el shard c a tu beneficiario.

+
+

si estás ocupado, lo entiendo. pero por favor, hacé clic ahora:

+ estoy aquí, resetear el timer +

hasta pronto,
deadhand protocol

+ + """, + "pt": f""" +

olá,

+

estou um pouco preocupado. não temos notícias suas há 60 dias.

+
+

só faltam 30 dias.

+

se não clicar no link abaixo dentro de um mês, o sistema vai assumir o pior e enviar automaticamente o shard c para seu beneficiário.

+
+

se estiver ocupado, entendo. mas por favor, clique agora:

+ estou aqui, resetar o timer +

até logo,
deadhand protocol

+ + """, + "de": f""" +

hey,

+

ich mache mir ein wenig Sorgen. wir haben seit 60 Tagen nichts von dir gehört.

+
+

nur noch 30 Tage.

+

wenn du den Link unten nicht innerhalb des nächsten Monats klickst, nimmt unser System das Schlimmste an und sendet Shard C automatisch an deinen Begünstigten.

+
+

wenn du nur beschäftigt bist, verstehe ich das. aber bitte, klick jetzt:

+ ich bin hier, Timer zurücksetzen +

bis bald,
deadhand protocol

+ + """, + "fr": f""" +

bonjour,

+

je commence à m'inquiéter. nous n'avons pas de nouvelles depuis 60 jours.

+
+

plus que 30 jours.

+

si vous ne cliquez pas sur le lien ci-dessous dans le mois qui vient, notre système supposera le pire et enverra automatiquement le shard c à votre bénéficiaire.

+
+

si vous êtes simplement occupé, je comprends tout à fait. mais s'il vous plaît, cliquez maintenant :

+ je suis là, réinitialiser le timer +

à bientôt,
deadhand protocol

+ + """, + "ja": f""" +

こんにちは、

+

少し心配しています。60日間連絡がありません。

+
+

残り30日です。

+

来月中にリンクをクリックしない場合、システムは最悪の事態を想定し、shard cを受取人に自動送信します。

+
+

お忙しいのは理解できます。でも今すぐクリックしてください:

+ ここにいます、タイマーをリセット +

またお話しましょう、
deadhand protocol

+ + """, + } + return _html_wrap(bodies.get(lang, bodies["en"])) + + +# --------------------------------------------------------------------------- +# DEATH EMAIL (to beneficiary) +# --------------------------------------------------------------------------- + +def get_death_email(lang: str, owner_email: str, shard_c_value: str) -> str: + bodies = { + "en": f""" +

a message from Deadhand.

+

hello,

+

i'm writing to you because 90 days ago, {owner_email} entrusted our system to reach out to you if we stopped hearing from them.

+

we haven't received a heartbeat check-in from them in three months. as per their explicit instructions, i am now releasing the final piece of their digital legacy to you.

+

this is shard c. it's one of three pieces needed to access their crypto assets. you should already have shard b (a document they gave you).

+
shard c value:
{shard_c_value}
+
+

how to use this:

+
    +
  1. locate shard b (the one they gave you).
  2. +
  3. go to deadhandprotocol.com/recover.
  4. +
  5. enter both shard b and shard c into the tool.
  6. +
  7. the tool will reconstruct their original seed phrase.
  8. +
+
+

my deepest condolences. i built Deadhand so families wouldn't be locked out of their loved ones' assets during difficult times.

+

with respect,
deadhand protocol

+
+

protect your own legacy

+

you've just seen how Deadhand works. set up your own trustless switch in 5 minutes.

+ create your vault +
+ + """, + "es": f""" +

un mensaje de Deadhand.

+

hola,

+

te escribo porque hace 90 días, {owner_email} le encargó a nuestro sistema que te contactara si dejábamos de tener noticias de esa persona.

+

no hemos recibido ningún heartbeat en tres meses. según sus instrucciones explícitas, te entrego ahora la última pieza de su legado digital.

+

esto es el shard c. es una de las tres piezas necesarias para acceder a sus criptoactivos. ya deberías tener el shard b (un documento que te entregaron).

+
valor del shard c:
{shard_c_value}
+
+

cómo usarlo:

+
    +
  1. encontrá el shard b (el que te dieron).
  2. +
  3. entrá a deadhandprotocol.com/recover.
  4. +
  5. ingresá el shard b y el shard c en la herramienta.
  6. +
  7. la herramienta reconstruirá la seed phrase original.
  8. +
+
+

mis más sinceras condolencias. construí Deadhand para que las familias no quedaran sin acceso a los activos de sus seres queridos en momentos difíciles.

+

con respeto,
deadhand protocol

+
+

protegé tu propio legado

+

acabás de ver cómo funciona Deadhand. configurá tu propio switch en 5 minutos.

+ crear tu vault +
+ + """, + "pt": f""" +

uma mensagem do Deadhand.

+

olá,

+

estou escrevendo porque há 90 dias, {owner_email} confiou ao nosso sistema que entrasse em contato com você se parássemos de receber notícias dela.

+

não recebemos nenhum heartbeat em três meses. conforme suas instruções explícitas, estou liberando agora a peça final de seu legado digital para você.

+

este é o shard c. é uma das três peças necessárias para acessar seus criptoativos. você já deve ter o shard b (um documento que te foi entregue).

+
valor do shard c:
{shard_c_value}
+
+

como usar:

+
    +
  1. localize o shard b (o que te foi dado).
  2. +
  3. acesse deadhandprotocol.com/recover.
  4. +
  5. insira o shard b e o shard c na ferramenta.
  6. +
  7. a ferramenta vai reconstruir a seed phrase original.
  8. +
+
+

meus sinceros pêsames. construí o Deadhand para que as famílias não ficassem sem acesso aos ativos de seus entes queridos em momentos difíceis.

+

com respeito,
deadhand protocol

+
+

proteja seu próprio legado

+

você acabou de ver como o Deadhand funciona. configure seu próprio switch em 5 minutos.

+ criar seu vault +
+ + """, + "de": f""" +

eine Nachricht von Deadhand.

+

hallo,

+

ich schreibe dir, weil {owner_email} vor 90 Tagen unserem System die Aufgabe übertragen hat, dich zu kontaktieren, falls wir nichts mehr von ihr/ihm hören.

+

wir haben seit drei Monaten keinen Heartbeat erhalten. gemäß ihren/seinen ausdrücklichen Anweisungen übergebe ich dir jetzt das letzte Stück ihres/seines digitalen Erbes.

+

das ist Shard C. es ist eines von drei Teilen, die benötigt werden, um auf ihre/seine Krypto-Assets zuzugreifen. du solltest bereits Shard B haben (ein Dokument, das sie/er dir übergeben hat).

+
Shard C Wert:
{shard_c_value}
+
+

so verwendest du es:

+
    +
  1. finde Shard B (den sie/er dir gegeben hat).
  2. +
  3. gehe zu deadhandprotocol.com/recover.
  4. +
  5. gib Shard B und Shard C in das Tool ein.
  6. +
  7. das Tool rekonstruiert die ursprüngliche Seed-Phrase.
  8. +
+
+

mein aufrichtiges Beileid. ich habe Deadhand gebaut, damit Familien nicht vom Zugang zu den Assets ihrer Lieben ausgeschlossen werden.

+

mit Respekt,
deadhand protocol

+
+

schütze dein eigenes Erbe

+

du hast gerade gesehen, wie Deadhand funktioniert. richte deinen eigenen Switch in 5 Minuten ein.

+ vault erstellen +
+ + """, + "fr": f""" +

un message de Deadhand.

+

bonjour,

+

je vous écris parce qu'il y a 90 jours, {owner_email} a confié à notre système de vous contacter si nous n'avions plus de nouvelles.

+

nous n'avons reçu aucun heartbeat depuis trois mois. conformément à ses instructions explicites, je vous remets maintenant la dernière pièce de son héritage numérique.

+

voici le shard c. c'est l'une des trois pièces nécessaires pour accéder à ses actifs crypto. vous devriez déjà avoir le shard b (un document qu'il/elle vous a remis).

+
valeur du shard c :
{shard_c_value}
+
+

comment l'utiliser :

+
    +
  1. localisez le shard b (celui qu'on vous a donné).
  2. +
  3. rendez-vous sur deadhandprotocol.com/recover.
  4. +
  5. entrez le shard b et le shard c dans l'outil.
  6. +
  7. l'outil reconstruira la phrase seed originale.
  8. +
+
+

mes plus sincères condoléances. j'ai créé Deadhand pour que les familles ne soient pas exclues des actifs de leurs proches en période difficile.

+

avec respect,
deadhand protocol

+
+

protégez votre propre héritage

+

vous venez de voir comment Deadhand fonctionne. configurez votre propre switch en 5 minutes.

+ créer votre vault +
+ + """, + "ja": f""" +

Deadhandからのメッセージ。

+

こんにちは、

+

90日前、{owner_email}が連絡を絶った場合にあなたへ連絡するよう、私たちのシステムに委託していました。

+

3ヶ月間ハートビートを受信していません。明示的な指示に従い、デジタル遺産の最後のピースをあなたにお渡しします。

+

これはshard cです。暗号資産にアクセスするために必要な3つのピースのうちの1つです。すでにshard b(渡されたドキュメント)をお持ちのはずです。

+
shard cの値:
{shard_c_value}
+
+

使い方:

+
    +
  1. shard b(渡されたもの)を見つけてください。
  2. +
  3. deadhandprotocol.com/recoverにアクセスしてください。
  4. +
  5. ツールにshard bとshard cを入力してください。
  6. +
  7. ツールが元のシードフレーズを復元します。
  8. +
+
+

心よりお悔やみ申し上げます。困難な時期に大切な人の資産にアクセスできなくならないよう、Deadhandを作りました。

+

敬意を込めて、
deadhand protocol

+
+

自分のレガシーを守る

+

Deadhandの仕組みを見ていただきました。5分で自分のスイッチを設定できます。

+ vaultを作成する +
+ + """, + } + return _html_wrap(bodies.get(lang, bodies["en"])) \ No newline at end of file diff --git a/app/main.py b/app/main.py index f5f2b30..0538607 100644 --- a/app/main.py +++ b/app/main.py @@ -65,6 +65,15 @@ async def get_btc_price(): from .models import User from .services import send_email from .crypto import encrypt_shard, decrypt_shard, encrypt_token, decrypt_token +from .emails import ( + get_language_from_email, + get_welcome_email, + get_cancellation_email, + get_reminder_30d_email, + get_warning_60d_email, + get_death_email, + SUBJECTS, +) # Create database tables Base.metadata.create_all(bind=engine) @@ -452,32 +461,10 @@ async def stripe_webhook(request: Request, db: Session = Depends(get_db)): db.commit() # Send human-centered cancellation email - cancellation_html = f""" - - - - - - -

hey,

-

i just got word that your subscription was cancelled. your vault is now deactivated.

-

i'm not going to send you a "please come back" survey or a discount code to win you over. i just want to say thanks for trusting deadhand for a while.

-

if you ever want to protect your family again, you know where to find me.

+ lang = user.preferred_language or get_language_from_email(user.email) + cancellation_html = get_cancellation_email(lang) + send_email(user.email, SUBJECTS["cancellation"][lang], cancellation_html) -

take care,

-

deadhand protocol

- - - - - """ - send_email(user.email, "your deadhand vault has been deactivated", cancellation_html) - return {"status": "success"} @app.get("/", response_class=HTMLResponse) @@ -934,7 +921,8 @@ async def create_vault( encrypted_shard = encrypt_shard(shard_c, heartbeat_token) if not user: - user = User(email=email) + lang = get_language_from_email(email) + user = User(email=email, preferred_language=lang) db.add(user) # Encrypt heartbeat_token to protect it in DB @@ -969,80 +957,15 @@ async def create_vault( welcome_cal_url = f"https://www.google.com/calendar/render?action=TEMPLATE&text={cal_title}&dates={cal_date}/{cal_end}&details={cal_details}&sf=true&output=xml" # Send human-centered "Chewy-style" welcome email - welcome_html = f""" - - - - - - -
-

it's not just a welcome email.

- -

hey there,

- -

i'm max, the founder of deadhand.

- -

i could have sent you a shiny, corporate html template with "action required" in the subject. but deadhand isn't a typical app, and you aren't a typical user.

- -

you just made a hard choice. thinking about what happens "after" isn't exactly fun. but the fact that you're here means you deeply care about someone and you want to protect them no matter what. that’s a powerful thing, and it deserves more than a form letter.

- -

in a digital world that's getting colder by the second, i wanted to give you something "handmade." since my actual drawing skills stopped improving in kindergarten, i used a specialized ai to help me create a "photo" of a crayon drawing i made while thinking about this project. it’s imperfect, it's a bit silly, but it’s real to me.

- -
- a drawing of a family for you -
- -

i want you to know that on the other side of this complex math is a real person who understands the weight of what you're setting up. i don't take that trust lightly.

- -
- vault active for: {email}
- beneficiary: {beneficiary_email}
- system: 2-of-3 shamir's secret sharing
- status: secured -
- -

take a breath. your family is safe now. there’s no rush to do anything else right now. just keep your shard a safe, and make sure your beneficiary has shard b.

- -

one critical thing: to make sure you're still with us, we need a "heartbeat." click the link below once just to verify you can access it. it resets your 90-day timer.

- - verify my heartbeat & reset timer - -

pro tip: set a reminder so you don't forget. add a reminder to my google calendar (for 30 days from now).

- -
- handwritten note on a napkin: your family is safe now -
- -

this is my personal email. if you have a question, a fear, or just want to tell me how your setup went, just reply. i read them. i answer them.

- -

deeply grateful you're here,

- -

max
- founder of deadhand
- (the guy who sends you crayon drawings)

- - -
- - - """ - - send_email(email, "not just a welcome email (and a drawing for you)", welcome_html) - - return templates.TemplateResponse("success.html", {"request": request}) + lang = get_language_from_email(email) + heartbeat_link = f"https://deadhandprotocol.com/heartbeat/{user.id}/{heartbeat_token}" + welcome_html = get_welcome_email(lang, email, beneficiary_email, heartbeat_link) + send_email(email, SUBJECTS["welcome"][lang], welcome_html) + + return templates.TemplateResponse("success.html", { + "request": request, + "calendar_url": welcome_cal_url + }) @app.get("/heartbeat/{user_id}/{token}", response_class=HTMLResponse) async def heartbeat(request: Request, user_id: int, token: str, db: Session = Depends(get_db)): @@ -1136,76 +1059,18 @@ async def check_heartbeats(db: Session = Depends(get_db)): # 30-day reminder - chewy style if 29 <= days_since_heartbeat <= 31: - reminder_html = f""" - - - - - - -

hey,

-

it's been 30 days since we last heard from you. i'm just checking in to make sure everything is okay.

-

could you click the link below? it just tells our system you're still with us and resets your timer. it takes two seconds.

- - i'm still here - -

pro tip: if you're busy now, add a reminder to your calendar for tomorrow so you don't forget.
- add reminder for tomorrow

- -

if you don't click it, no big deal for now. i'll check in again in another 30 days. but after 90 days of silence, we'll have to send shard c to your beneficiary.

- -

stay safe out there,

-

deadhand protocol

- - - - - """ - send_email(user.email, "quick check-in from Deadhand", reminder_html) - results["reminders_30d"] += 1 - + lang = user.preferred_language or get_language_from_email(user.email) + heartbeat_link = f"https://deadhandprotocol.com/heartbeat/{user.id}/{heartbeat_token_plain}" + reminder_html = get_reminder_30d_email(lang, heartbeat_link) + send_email(user.email, SUBJECTS["reminder_30d"][lang], reminder_html) + results["reminders_30d"] += 1 + # 60-day warning - urgent but human elif 59 <= days_since_heartbeat <= 61: - warning_html = f""" - - - - - - -

hey,

-

i'm getting a little worried. we haven't heard from you in 60 days.

- -
-

just 30 days left.

-

if you don't click the link below within the next month, our system will assume the worst and automatically send shard c to your beneficiary.

-
- -

if you're just busy, i totally get it. but please, click this now so we don't worry your family unnecessarily:

- - i'm here, reset the timer - -

talk soon,

-

deadhand protocol

- - - - - """ - send_email(user.email, "urgent: we haven't heard from you in 60 days", warning_html) + lang = user.preferred_language or get_language_from_email(user.email) + heartbeat_link = f"https://deadhandprotocol.com/heartbeat/{user.id}/{heartbeat_token_plain}" + warning_html = get_warning_60d_email(lang, heartbeat_link) + send_email(user.email, SUBJECTS["warning_60d"][lang], warning_html) results["warnings_60d"] += 1 # 90-day death trigger @@ -1229,65 +1094,10 @@ async def check_heartbeats(db: Session = Depends(get_db)): shard_c_value = user.shard_c user.is_dead = True - death_html = f""" - - - - - - -

a message from Deadhand.

- -

hello,

-

i'm writing to you because 90 days ago, {user.email} entrusted our system to reach out to you if we stopped hearing from them.

- -

we haven't received a heartbeat check-in from them in three months. as per their explicit instructions, i am now releasing the final piece of their digital legacy to you.

- -

this is shard c. it's one of three pieces needed to access their crypto assets. if they followed our setup guide, you should already have shard b (likely a printed document or a digital file they gave you).

- -
- shard c value:
- {shard_c_value} -
- -
-

how to use this:

-
    -
  1. locate shard b (the one they gave you).
  2. -
  3. go to deadhandprotocol.com/recover.
  4. -
  5. enter both shard b and shard c into the tool.
  6. -
  7. the tool will reconstruct their original seed phrase for you.
  8. -
-
- -

my deepest condolences for whatever situation has led to this email. i built Deadhand specifically so that people wouldn't have to worry about their loved ones being locked out of their hard-earned assets during difficult times.

- -

i hope this tool helps you in some small way.

- -

with respect,

-

deadhand protocol

- -
-

protect your own legacy

-

you've just seen how Deadhand works. if you have crypto, don't leave your family in the dark. set up your own trustless switch in 5 minutes.

- create your vault -
- - - - - """ - send_email(user.beneficiary_email, "important: digital recovery key for " + user.email, death_html) + lang = user.preferred_language or get_language_from_email(user.beneficiary_email) + death_html = get_death_email(lang, user.email, shard_c_value) + subject = SUBJECTS["death"][lang].format(owner_email=user.email) + send_email(user.beneficiary_email, subject, death_html) results["deaths_90d"] += 1 except Exception as e: diff --git a/app/models.py b/app/models.py index 5e7667e..df42172 100644 --- a/app/models.py +++ b/app/models.py @@ -28,5 +28,7 @@ class User(Base): stripe_subscription_id = Column(String, nullable=True, index=True) # sub_xxx (for annual) plan_type = Column(String, default="lifetime") # "annual" or "lifetime" is_active = Column(Boolean, default=True) # False if subscription cancelled + + preferred_language = Column(String, default="en") created_at = Column(DateTime(timezone=True), server_default=func.now())