From ef6066d8d4cb8d15984c6d34bc42dcf37f54ee3a Mon Sep 17 00:00:00 2001
From: jolah1
Date: Sat, 4 Jul 2026 09:28:35 +0100
Subject: [PATCH] fix: honesty and plain-words pass from the UX review (part 1
+ 2)
Landing (#204 review batch):
- trust row "Your key / Never stored" -> "Locked by your password"
(the sealed xprv IS stored server-side, encrypted; never claim otherwise)
- FAQ no longer promises editing the waiting period or heir on a live
vault; describes the real path (drain + new vault, add heirs anytime)
- deposit FAQ: funds go to the vault's own address, not "stays in your
wallet"
- shutdown FAQ + RecoveryGuide (#263): tell the two-files story - the
owner's spare key opens with their password, the heir's file opens
with the five-word code kept beside it
- OP_CSV jargon dropped
Claim page:
- fee field folded behind "Advanced: change the network fee" in the
password-vault and guardian variants (matching the derived variant)
- one wallet list everywhere: Blink, Bitnob, Wallet of Satoshi
(Cash App/Phoenix dropped; not the audience)
- success link: "Watch it arrive" instead of naming mempool.space
Other pages:
- RecoveryKitPage: storage advice now matches the kit file (offline
first, email least private)
- StatusPage: "keys only you and your heir can use" -> "locked by
keys, not by our uptime" (Door A honesty rule)
- LightningCheckin: "Minting invoice..." -> "Getting your check-in ready..."
- HeirVideoMessage: verification copy in plain words
- CheckinPortal (legacy): "credentials"/"owner token" -> plain words
Emails (scheduler):
- reminder no longer promises "one more email" when the alarm phase
rightly sends daily reminders; says so honestly
- "day(s)" -> real singular/plural
- "pay the tiny N-sat Lightning invoice" -> "pay N sats from any
Lightning wallet" in all three owner emails + both push bodies
Co-Authored-By: Claude Opus 4.8
---
crates/ghostkey-server/src/scheduler.rs | 36 ++++----
ghostkey-web/src/CheckinPortal.tsx | 11 ++-
ghostkey-web/src/ClaimPage.tsx | 104 +++++++++++++-----------
ghostkey-web/src/HeirVideoMessage.tsx | 8 +-
ghostkey-web/src/Landing.tsx | 10 +--
ghostkey-web/src/LightningCheckin.tsx | 2 +-
ghostkey-web/src/RecoveryGuide.tsx | 56 ++++++++-----
ghostkey-web/src/RecoveryKitPage.tsx | 8 +-
ghostkey-web/src/StatusPage.tsx | 6 +-
9 files changed, 137 insertions(+), 104 deletions(-)
diff --git a/crates/ghostkey-server/src/scheduler.rs b/crates/ghostkey-server/src/scheduler.rs
index dbe6468..c1482c0 100644
--- a/crates/ghostkey-server/src/scheduler.rs
+++ b/crates/ghostkey-server/src/scheduler.rs
@@ -435,25 +435,28 @@ async fn enqueue_alarm_escalation(
let amount_sat = crate::lightning::heartbeat_amount_sat();
let one_tap_block = match mint_or_reuse_one_tap_token(state, vault_id, now_iso).await? {
Some(token) => format!(
- "Tap this link and pay the tiny {amount_sat}-sat Lightning \
- invoice to check in:\n\n\
+ "Tap this link and pay {amount_sat} sats from any Lightning \
+ wallet to check in:\n\n\
{base}/#/checkin-link/{vault_id}/{token}\n\n"
),
None => String::new(),
};
+ // "1 days" reads like a robot wrote it, at the exact moment the
+ // email most needs to be taken seriously.
+ let days_word = if days_left == 1 { "day" } else { "days" };
let (subject, lead) = match count_so_far {
0 => (
- format!("You missed a check-in. {days_left} days until your heir is notified"),
+ format!("You missed a check-in. {days_left} {days_word} until your heir is notified"),
"This is the first daily reminder.".to_string(),
),
n if n < 7 => (
- format!("{days_left} days left to check in before your heir is contacted"),
+ format!("{days_left} {days_word} left to check in before your heir is contacted"),
format!("You've now missed {} daily reminders.", n + 1),
),
_ => (
format!(
- "Last few days: {days_left} days until your heir is notified about {display_label}"
+ "Last few days: {days_left} {days_word} until your heir is notified about {display_label}"
),
"This is one of the final reminders.".to_string(),
),
@@ -465,7 +468,7 @@ async fn enqueue_alarm_escalation(
{one_tap_block}\
You can also open the dashboard on any device:\n\n\
{base}/#/checkin\n\n\
- If we don't hear from you within {days_left} day(s), your heir \
+ If we don't hear from you within {days_left} {days_word}, your heir \
will receive a claim link for this vault. You can stop that \
instantly by checking in.\n\n\
From GhostKey\n"
@@ -756,17 +759,16 @@ async fn enqueue_pre_deadline_reminder(
"Hello,\n\n\
A quick reminder that {display_label} needs a check-in by \
{deadline_friendly}. That's about 24 hours from now.\n\n\
- Tap this link and pay the tiny {amount_sat}-sat Lightning \
- invoice. The payment is your proof you're still here, and it \
+ Tap this link and pay {amount_sat} sats from any Lightning \
+ wallet. The payment is your proof you're still here, and it \
resets the countdown.\n\n\
{one_tap_url}\n\n\
- You can pay from any Lightning wallet. If you'd rather not from \
- this email, open the dashboard on any device and check in \
- there:\n\n\
+ If you'd rather not pay from this email, open the dashboard \
+ on any device and check in there:\n\n\
{base}/#/checkin\n\n\
- If we don't hear from you by the deadline, you'll get one more \
- email, and then your heir will be contacted after the \
- grace period.\n\n\
+ If we don't hear from you by the deadline, we'll keep \
+ reminding you every day through the grace period. Only after \
+ that would your heir be contacted.\n\n\
If this email reached you by mistake, you can ignore it.\n\n\
From GhostKey\n"
);
@@ -789,7 +791,7 @@ async fn enqueue_pre_deadline_reminder(
"title": title,
"body": format!(
"{display_label} needs a check-in by {deadline_friendly}. \
- Pay a tiny {amount_sat}-sat Lightning invoice to check in."
+ Pay {amount_sat} sats from any Lightning wallet to check in."
),
"url": one_tap_url,
})
@@ -1092,8 +1094,8 @@ async fn enqueue_alarm_owner(
let amount_sat = crate::lightning::heartbeat_amount_sat();
let one_tap_block = match &one_tap_url {
Some(url) => format!(
- "Tap this link and pay the tiny {amount_sat}-sat Lightning \
- invoice to check in. The payment is your proof you're still \
+ "Tap this link and pay {amount_sat} sats from any Lightning \
+ wallet to check in. The payment is your proof you're still \
here and resets the clock.\n\n\
{url}\n\n"
),
diff --git a/ghostkey-web/src/CheckinPortal.tsx b/ghostkey-web/src/CheckinPortal.tsx
index f2e6d9f..023c23e 100644
--- a/ghostkey-web/src/CheckinPortal.tsx
+++ b/ghostkey-web/src/CheckinPortal.tsx
@@ -198,20 +198,19 @@ export function CheckinPortal({ initialId }: { initialId?: string }) {
{state.kind === "no-credentials" && (
- This device doesn't have the credentials for vault{" "}
+ This device doesn't remember vault{" "}
{state.id.slice(0, 8)}….
- If you set this vault up with a password, sign in with
+ If you set your vault up with a password, sign in with
your email and password instead.{" "}
- go to sign in
+ Go to sign in
- . For legacy vaults, check in from the browser you used
- to set the vault up; the owner token only lives on that
- device.
+ . For older vaults, check in from the browser you used at
+ setup; that's the only device that can open this one.
Open any Bitcoin wallet and tap Receive. A
- Lightning wallet works too. Apps like Wallet of Satoshi, Bitnob,
- Cash App, or Phoenix each give you a Bitcoin address that adds the
- money to your balance. Copy the long address that starts with{" "}
+ Lightning wallet works too. Apps like Blink, Bitnob, or Wallet of
+ Satoshi each give you a Bitcoin address that adds the money to
+ your balance. Copy the long address that starts with{" "}
{bech32PrefixFor(sealed.network)}{" "}
and paste it below.
@@ -1291,26 +1296,31 @@ function PasswordVaultClaim({
-
Paste any Bitcoin address you control on the {networkLabel(params.network)}.{" "}
- A Lightning wallet works too. Apps like Wallet of Satoshi, Bitnob,
- Cash App, or Phoenix each give you a Bitcoin address that adds the
- money to your balance. {walletExamplesInline(params.network)}.
+ A Lightning wallet works too. Apps like Blink, Bitnob, or Wallet of
+ Satoshi each give you a Bitcoin address that adds the money to your
+ balance. {walletExamplesInline(params.network)}.
@@ -1992,7 +2002,7 @@ function BroadcastSuccess({ result }: { result: BroadcastClaimResponse }) {
rel="noreferrer noopener"
className="font-display text-sm font-bold tracking-tight text-accent underline underline-offset-2"
>
- Watch it confirm on mempool.space ↗
+ Watch it arrive ↗
✓
- Verified. Recorded by the owner of this vault, and untampered.
+ Verified. Recorded by the owner of this vault, and it hasn't been
+ changed since.
) : (
- This message played, but its signature did not match the vault
- owner's key. It may have been altered. Treat it with caution.
+ This message played, but we could not confirm it really came from
+ the person who set up this vault. It may have been changed. Treat
+ it with caution.
)}
diff --git a/ghostkey-web/src/Landing.tsx b/ghostkey-web/src/Landing.tsx
index b9082c1..c1b565b 100644
--- a/ghostkey-web/src/Landing.tsx
+++ b/ghostkey-web/src/Landing.tsx
@@ -112,7 +112,7 @@ function Hero({ onNavigate }: Props) {
function TrustRow() {
const items = [
{ strong: "0", sub: "Third parties" },
- { strong: "Your key", sub: "Never stored" },
+ { strong: "Your key", sub: "Locked by your password" },
{ strong: "On-chain", sub: "No company can freeze it" },
];
return (
@@ -249,7 +249,7 @@ const REASONS: Reason[] = [
{
tag: "Precise",
title: "On-chain timelocks",
- body: "Waiting periods are enforced by Bitcoin itself using OP_CSV. Once set, no one can move funds before the timer runs out. Not us, not them, not anyone.",
+ body: "Waiting periods are enforced by Bitcoin itself. Once set, no one can move the funds to your heir before the timer runs out. Not us, not them, not anyone.",
},
{
tag: "Trustless",
@@ -478,7 +478,7 @@ const FAQS = [
},
{
q: "Can I change my heir after creating a vault?",
- a: "Yes, while you're still checking in. From the dashboard you can change the heir, the waiting period, or how often you check in. The change takes effect the next time you update the vault.",
+ a: "You can add another heir at any time, each with their own share. To change who inherits an existing share, or to change its waiting period, send that share back to your own wallet from the dashboard (one transaction) and set up a fresh vault with the new details. While you keep checking in, nothing ever moves without you.",
},
{
q: "What does a check-in cost?",
@@ -494,7 +494,7 @@ const FAQS = [
},
{
q: "What kinds of Bitcoin can I deposit?",
- a: "Any Bitcoin you already hold. You don't send it to us. It stays in your wallet, with the inheritance rules attached to it.",
+ a: "Any Bitcoin you already hold. You send it from your wallet to your vault's own Bitcoin address, one whose rules already include your heir. We never hold it and can't spend it.",
},
{
q: "Is this a legal will?",
@@ -506,7 +506,7 @@ const FAQS = [
},
{
q: "What if GhostKey shuts down?",
- a: "Your heir can still claim. When you set up, you download a recovery kit: one file with everything needed to claim the Bitcoin straight from the Bitcoin network. If GhostKey ever disappears, your heir, or anyone helping them, uses that file to unlock the funds without us. Keep it with your important papers.",
+ a: "Your heir can still claim. At setup you save two files: your own spare key, which opens with your password, and a file for your heir, which opens with a short code kept beside it. If GhostKey ever disappears, your heir opens their file and it walks them through claiming straight from the Bitcoin network, no GhostKey needed. Keep both with your important papers.",
},
];
diff --git a/ghostkey-web/src/LightningCheckin.tsx b/ghostkey-web/src/LightningCheckin.tsx
index f2004fa..2919bae 100644
--- a/ghostkey-web/src/LightningCheckin.tsx
+++ b/ghostkey-web/src/LightningCheckin.tsx
@@ -196,7 +196,7 @@ export function LightningCheckin({
Your heir can still claim. The rules that release your Bitcoin
live on the Bitcoin network, not on our servers. At setup you
- download one recovery file that holds everything needed to claim
- without us. Keep it with your important papers and tell your heir
- where it is.
+ save two files: your own spare key, and a file made for your
+ heir with its own short unlock code. Keep the heir's file and
+ its code together with your important papers, and tell your
+ heir where they are.
- At setup you download a single file, named something like{" "}
+ Your spare key is a file named something like{" "}
ghostkey-recovery-yourvault.html
- . It contains the vault's rules and your own encrypted key. It
- does not need GhostKey, an account, or an internet connection to
- do its job.
+ . It opens with the same password you sign in with, and it's for
+ you: if you ever lose access to GhostKey, it reaches your money.
+ Your heir cannot open this one, and that's on purpose. You never
+ have to write your password down.
- Treat it like a key to a safe. Store a copy somewhere safe and
- lasting, and make sure the people who would need it know where to
- find it. If you ever misplace it, you can sign in and download a
- fresh copy from the{" "}
+ Your heir's file is named something like{" "}
+
+ ghostkey-for-ada.html
+
+ . It opens with a short code of five simple words, shown to you
+ once at setup. Keep the file and its code together, somewhere
+ your heir will look when the time comes: with your will, your
+ important papers, or someone you both trust. Keeping the code
+ beside the file is safe, because Bitcoin's own timer keeps the
+ file powerless while you're alive and checking in.
+
+
+ Neither file needs GhostKey, an account, or an internet
+ connection to do its job. If you misplace your own spare key,
+ sign in and download a fresh copy from the{" "}