Skip to content

Commit 63548bc

Browse files
ralyodioclaude
andcommitted
fix(join): only mint the $99 CoinPay charge on explicit opt-in
offerPremium ran on every join@ and called CreatePremiumCharge just to show the pitch, so CoinPay minted a $99 payment for everyone who connected. Show the pitch with no charge, then create the payment only when the member types "yes" at the prompt (the SSH equivalent of clicking "Become a paid member"). Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent 94ac404 commit 63548bc

1 file changed

Lines changed: 53 additions & 32 deletions

File tree

cmd/agentbbs/main.go

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ func (a *app) handleJoin(s ssh.Session) {
707707
wish.Println(s, "\n"+strings.Join(includes, "\n"))
708708

709709
// 2) Founding Lifetime ($99 one-time): custom domains + Tor shell.
710-
a.offerPremium(s, &u)
710+
a.offerPremium(s, in, &u)
711711
_ = s.Exit(0)
712712
}
713713

@@ -921,20 +921,20 @@ func (a *app) showPremiumWelcome(s ssh.Session, u store.User) {
921921
}
922922

923923
// offerPremium pitches the $99 Founding Lifetime membership — custom domains and
924-
// the Tor shell. When CoinPay can mint a charge in-session it shows the exact
925-
// amount and deposit address; otherwise it falls back to a pay command.
926-
// Non-blocking: the member pays out of band and perks unlock on their next
927-
// connect (or re-running join@).
928-
func (a *app) offerPremium(s ssh.Session, u *store.User) {
924+
// the Tor shell — and only mints a CoinPay charge if the member explicitly opts
925+
// in at the prompt. Showing the pitch must NOT create a payment: minting on
926+
// every join@ produced a $99 invoice for everyone who connected. Non-blocking:
927+
// the member pays out of band and perks unlock on their next connect (or
928+
// re-running join@).
929+
func (a *app) offerPremium(s ssh.Session, in *bufio.Reader, u *store.User) {
929930
// Maybe they already paid (e.g. re-ran join@ after paying).
930931
if a.ensurePremium(u) {
931932
a.showPremiumWelcome(s, *u)
932933
return
933934
}
934-
ref := payments.PremiumReference(u.PubKeyFP)
935935

936-
lines := []string{
937-
"",
936+
// Pitch only — no charge is created here.
937+
wish.Println(s, "\n"+strings.Join([]string{
938938
" ★ Founding Lifetime Member — $" + payments.PremiumAmount() + ", one-time",
939939
" Only the first " + payments.FoundingCap + " accounts. Pay once, keep it for life.",
940940
"",
@@ -944,33 +944,44 @@ func (a *app) offerPremium(s ssh.Session, u *store.User) {
944944
" • custom domains point yourdomain.com at your homepage",
945945
" • Tor a “Tor shell” in your pod — everything over Tor",
946946
" • locked-in price founding rate is yours for life — never renew, never pay again",
947-
"",
947+
}, "\n"))
948+
949+
// Explicit opt-in. Anything but yes leaves with no payment created.
950+
wish.Print(s, "\n Become a Founding member now? Type \"yes\" for a payment address [no]: ")
951+
line, err := readLine(s, in)
952+
if err != nil || !isYes(line) {
953+
wish.Println(s, "\n No problem — you're a free member. Want it later? Re-run: ssh join@"+a.host+"\n")
954+
return
948955
}
949-
if c, ok, err := payments.CreatePremiumCharge(ref); ok && err == nil {
950-
// Remember the payment id so a later connect can confirm settlement.
951-
if err := a.st.SetPremiumPayment(u.ID, c.ID); err != nil {
952-
log.Error("store premium payment id", "err", err)
953-
}
954-
amount := "$" + payments.PremiumAmount() + " " + payments.PremiumCurrency()
955-
if c.CryptoAmount != "" {
956-
cur := c.Currency
957-
if cur == "" {
958-
cur = strings.ToUpper(payments.PremiumBlockchain())
959-
}
960-
amount += " (≈ " + c.CryptoAmount + " " + cur + ")"
961-
}
962-
lines = append(lines,
963-
" amount "+amount,
964-
" send to "+c.Address,
965-
)
966-
if c.QR != "" {
967-
lines = append(lines, " qr "+c.QR)
968-
}
969-
} else {
956+
957+
ref := payments.PremiumReference(u.PubKeyFP)
958+
c, ok, err := payments.CreatePremiumCharge(ref)
959+
if !ok || err != nil {
970960
if err != nil {
971961
log.Error("create premium charge", "err", err)
972962
}
973-
lines = append(lines, " Payment is temporarily unavailable — please try again shortly.")
963+
wish.Println(s, "\n Payment is temporarily unavailable — please try again shortly.\n")
964+
return
965+
}
966+
// Remember the payment id so a later connect can confirm settlement.
967+
if err := a.st.SetPremiumPayment(u.ID, c.ID); err != nil {
968+
log.Error("store premium payment id", "err", err)
969+
}
970+
amount := "$" + payments.PremiumAmount() + " " + payments.PremiumCurrency()
971+
if c.CryptoAmount != "" {
972+
cur := c.Currency
973+
if cur == "" {
974+
cur = strings.ToUpper(payments.PremiumBlockchain())
975+
}
976+
amount += " (≈ " + c.CryptoAmount + " " + cur + ")"
977+
}
978+
lines := []string{
979+
"",
980+
" amount " + amount,
981+
" send to " + c.Address,
982+
}
983+
if c.QR != "" {
984+
lines = append(lines, " qr "+c.QR)
974985
}
975986
lines = append(lines,
976987
"",
@@ -980,6 +991,16 @@ func (a *app) offerPremium(s ssh.Session, u *store.User) {
980991
wish.Println(s, strings.Join(lines, "\n"))
981992
}
982993

994+
// isYes reports whether a prompt line is an affirmative opt-in.
995+
func isYes(line string) bool {
996+
switch strings.ToLower(strings.TrimSpace(line)) {
997+
case "yes", "y":
998+
return true
999+
default:
1000+
return false
1001+
}
1002+
}
1003+
9831004
// notifySignup emails the operator the details of a newly verified signup.
9841005
// No-op when SMTP isn't configured. Subject is "bbs" per the operator's filter.
9851006
func (a *app) notifySignup(u store.User) {

0 commit comments

Comments
 (0)