From fc58bff91ae8e19efadfec62285f10a847e68535 Mon Sep 17 00:00:00 2001 From: shum Date: Mon, 24 Aug 2026 17:09:56 +0000 Subject: [PATCH] plan: give the badges flow named lanes and clear routing --- badges-flow-mvp.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badges-flow-mvp.svg b/badges-flow-mvp.svg index 2869a1e56b..c47efe949a 100644 --- a/badges-flow-mvp.svg +++ b/badges-flow-mvp.svg @@ -1 +1 @@ -Supporter badges - every screen, in the app and on the webinteraction design - one tree per surface, parent on the left, every branch to its rightplans/2026-08-21-badges-web-checkout.mdeditable SVGIn the appiOS idiom shown; Android and desktop differ only in chromeFor the operatorthe CLI behind the codesSupport SimpleXSimpleX is built by people who believeprivate messaging should not depend onadvertising. A badge helps pay for it.Why SimpleX is built this wayChoose your levelRedeem badge codeA1. Where it startsBoth actions ship today. On iOS and Play,'Redeem badge code' is not a secondary path -it is the only one, because store evidence isnot verified and the store purchase wasremoved rather than left charging for nothing.Choose your level‹SupportYour levelBoth levels remove the file size limit.Legend raises it further.Supporter$7 / month2 GB filesLegend$70 / month5 GB filesPerks are app constants, not catalog data -the catalog carries prices only.ContinueA2. Choose your levelPrices come from CRBadgeCatalog, never thestore products. One source, so the app and thesite cannot disagree about what a badge costs.Continue‹Your levelLegendPrepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%3 months is 2x the monthly price, 12 is 6x. Thesaving compares against the undiscounted totaland is never sent anywhere - the server pricesevery charge it makes.Continue in browserRedeem badge codeA3. How long - desktop and Android fossThe only builds with more than one way to pay,and the only ones that link out. 'Continue inbrowser' carries the answers so far as URLparameters.Continue‹LegendHow to payCheckout happens in your browser.Nothing is charged in the app.CardVisa, MastercardBitcoinon-chainMoneroon-chainContinue in browserOpens badges.simplex.chat with tier, monthsand method already chosen.A4. Only where there is a choiceDesktop and Android foss only. The storebuilds have no methods at all and never reachthis screen. The choice is a hint: the siteasks again if the parameter is unusable.The catalog fetch fails, or the operatordisabled the price.‹SupportYour levelBoth levels remove the file size limit.Supporter—price unavailableLegend—price unavailableCould not reach the badge serviceinternalContinueDisabled, not hidden - a level that exists butcannot be priced still exists.A2b. The service is unreachableEvery option renders disabled and nothingcrashes. The same state covers a tier whoseprice was disabled by the operator - the appcannot tell the two apart, and does not needto.On iOS and Play there is nothing to link outto, so the screen ends at redemption.‹Your levelLegendPrepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%Buy a code at badges.simplex.chat from anybrowser, then redeem it here.Redeem badge codeno purchase button on this buildA3b. How long - iOS and PlaySame screen, different ending. The storepurchase action is removed for the whole ofthis plan, and no link out replaces it: Appleand Google reject steering to outside purchasefor digital goods, so the screen simply endsat redemption.Redeem badge code - and the only route on astore build‹SupportRedeem codePaste the code from your receipt.SXB-9K2M4-7QRT1-XZ5WPasteRedeemFormats as you type and folds I and L to 1,O to 0 - the same normalisation the servicedoes, so a code read off a screen cannot failon ambiguity alone.A5. RedeemingTwenty Crockford characters in five groups.The check character is verified beforeanything is sent, so a mistyped code neverreaches the service and never costs a throttletoken.a valid code‹SettingsSupporter perksLegendshown on your profileENDS24 August 2027Prepaid months have no billing date. The badgeis reissued each month from the balance youalready paid for, and ends when it runs out.Add more monthsA6. After redeeming'Ends', never 'renews' - nothing recurs andthe copy must not imply it does. The date isthe paid-through date from the ledger balance,not the credential's expiry, which is a week-aligned internal the user never sees.on the profile‹ChatsAliceAAliceLegendlast seen recentlyIN GROUPSAAliceLegendBBobSupporterCCarolThe badge travels with the profile, so it showswherever a profile does - in chats, in a memberlist, on a contact card.A7. How everyone else sees itThe point of the whole flow. The badge is asigned credential carried in the profile andverified by the recipient's own client againstthe issuer key it already ships - not a flagthe server asserts, and not something a clientcan set for itself.Any error the service returns: code_invalid,code_used, code_expired or rate_limited.‹SupportRedeem codeThis code isn't validcode_invalidThis code has already been usedcode_usedThis code has expiredcode_expiredToo many attempts. Try again in 4 minutes.rate_limited - retryAfter 240Could not verify the credentialinternalOne message per error the service can return.Anything else, including a credential that failsto verify locally, shows the service's own textrather than a blank screen.A5b. Every redemption errorA revoked code is deliberatelyindistinguishable from an unknown one - bothsay 'isn't valid', so a guesser learns nothingfrom a revocation. Support tells them apartwith codes status; the wire does not.The last paid month has been issued and thebalance is zero.‹SettingsSupporter perksLegendended 24 August 2027Your badge has endedThe months you paid for have all been issued.The badge stopped showing on your profile;nothing was cancelled and nothing is owed.Add more monthsA6b. The balance runs outAn exhausted balance is not an error: theservice returns no credential and a zero-balance statement, and the app says soplainly. Prepaid means it simply stops.simplex-badge-service$simplex-badge-service codes issue \--type legend --months 12 --count 3 \--batch conf-2026 --expires 2027-08-24SXB-9K2M4-7QRT1-XZ5WB-3NHD8SXB-2W7XP-4LMQ8-9DKR3-6TVZ5SXB-8HYNC-1FGJ6-5PBW2-7QSXD3 codes issued. Printed once; only hashes stored.OP1. Minting codesCompensation and promotional codes have noorder behind them, so they are random ratherthan derived. Printed once to stdout and neveragain - the database holds only SHA-256hashes, which is what makes a stolen copyuseless.support$simplex-badge-service codes status \--ref K7M2Qorder 8f3a...c21e paid 24 Augbadge legend, 12 monthscode redeemed 2 Sep--reveal refused: code already redeemedOP2. Resolving a referenceThe customer quotes the five-characterreference from their receipt - never the orderid, never the code. --reveal is refused once acode is redeemed or revoked, on the samereasoning that stops the web page disclosingit.On the webbadges.simplex.chat - one centred column, max-width 560, the logo the only imagebadges.simplex.chatSupport SimpleXSimpleX has no ads, no user accounts and nothing to sell.A supporter badge helps pay for the people who build it.Supporter$7 / month - 2 GB filesLegend$70 / month - 5 GB filesChoose your levelAlready bought a code?Redeem it in the app: Settings, Supporter perks.The badge shows on your profile. Nothing renews byitself, and no account is created.simplex.chat/contactB1. The landing pageNot in the plan, and it should be. D3 specifies four screens starting at thetier question, which assumes everyone arrives from the app. Someone reachingthe site from a link or a search needs to know what this is before beingasked to choose a level - and someone who already has a code needs tellingthat redemption happens in the app, not here.Choose your levelbadges.simplex.chat/#/tierChoose your levelSupport SimpleX and lift the file size limit.Supporter$7 / month2 GB filesLegend$70 / month5 GB filesContinuesimplex.chat/contactB2. Question one: the levelThe wizard proper starts here. One centred column at max-width 560, generouswhitespace, accent #0053D0 - deliberately not the app's chrome, because it isnot the app. One question per screen, and a tier with no active price rendersdisabled rather than hidden.Continuebadges.simplex.chat/#/monthsHow long?Prepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%Continuesimplex.chat/contactB3. How longEvery figure comes from GET /api/catalog, the same totals the RPC cataloggives the app. The browser formats minor units and computes the comparisononly - it never multiplies a price by a month count.Continuebadges.simplex.chat/#/payHow would you like to pay?Card is handled by Stripe. Bitcoin and Moneroare on-chain, through BTCPay.CardVisa, MastercardBitcoinon-chainMoneroon-chainContinuesimplex.chat/contactB4. MethodA method whose provider is not configured is refused at checkout withprovider_unavailable rather than hidden here, so a half-configured deploymentfails loudly instead of quietly offering less.Continuebadges.simplex.chat/?tier=legend&months=12&pay=xmrCheck your orderLevelLegendDuration12 monthsMethodMoneroTotal$420.00Pay with MoneroYou will be shown an address and a QR code.simplex.chat/contactB5. Where the app hand-off landsA user arriving from the app skips B1 to B3 entirely:?tier=legend&months=12&pay=xmr answers all three questions, so the summary isthe first thing they see. POST /api/checkout then carries priceId, offerIdand method - never an amount, never a badge type. Both are derived server-side from the ids, so a tampered request cannot buy a Legend badge at aSupporter price.Pay with Monerobadges.simplex.chat/?order=8f3a...c21eSend 1.482 XMR$420.00 - this rate is held for 58:12MONERO ADDRESS48HqK2...9fRtWcCopyWaiting for the payment to confirmREFERENCEK7M2QBookmark this page - the address and the countdownboth live on this URL.simplex.chat/contactB6. Paying in cryptoPolls GET /api/order every 2s for the first minute then every 10s, and stopswhile the tab is hidden. A partial payment stays 'pending' - partials are thenormal first event of a multi-transaction payment, not a failure.it confirmsbadges.simplex.chat/?order=8f3a...c21e✓Paid. Here is your code.SXB-9K2M4-7QRT1-XZ5WB-3NHD8Copy codescan to carry it to your phoneREDEEM IT IN THE APPSimpleX -> Settings -> Supporter perks -> Redeem codeThis is the only place the code is shown.This page works until 23 September, and stops as soon asthe code is redeemed. The link is the code - treat it so.simplex.chat/contactB7. The code, onceDerived from the order id rather than stored, so a reload recomputes it whileonly its hash is ever at rest. Disclosure stops at redemption, at revocation,or 30 days after settlement - whichever comes first.Redeemed, revoked, or thirty days after settlement.badges.simplex.chat/?order=8f3a...c21eThis code has been redeemedThe code is no longer shown hereIt was redeemed on 2 September. If that was not you,get in touch.Legend, 12 monthspaid 24 AugustThe order stays; the capability does not.simplex.chat/contactB7b. The page outlives the codeDisclosure ends at redemption, at revocation, or 30 days after settlement.The order is still here because support resolves a reference against it - butthe URL stops being equivalent to the code, which is the whole point.The provider's section is missing from badge_service.ini.badges.simplex.chat/#/payHow would you like to pay?Monero is temporarily unavailableTry another method, or come back later.CardVisa, MastercardBitcoinon-chainMonerounavailableContinuesimplex.chat/contactB4b. A provider is not configured503 provider_unavailable from /api/checkout. The method is shown and disabledrather than omitted, so an operator who forgot a section sees it immediatelyinstead of wondering why nobody pays in Monero.Pay by card - Stripe returns herebadges.simplex.chat/?order=51c7...9d02Payment receivedWaiting for the card network to confirm.Still processingThis usually takes a few seconds. The page updates itself.If nothing happens for 15 minutes, we stop waitingand show you what to do next.simplex.chat/contactB5d. Coming back from the card flowStripe's success_url returns here with ?order=. The return is NOT proof ofpayment - only the webhook settles an order, so this screen polls rather thancelebrating. The cancel URL deliberately carries no order reference at all.The invoice expired with less than the full amount received.badges.simplex.chat/?order=8f3a...c21eThis invoice expired0.734 XMR arrived, which is not the full amountThe rate window has closed, so the shortfall is no longermeaningful. Quote the reference below and we will sort it out.REFERENCEK7M2QStart a new invoiceNever shows the code, and never the ?order= URL.simplex.chat/contactB6b. Expired, and underpaidThe one state that needs a human. An expired-but-paid invoice still settleslater - late on-chain settlement is routine and moves the order to paid - butan underpaid one stops here with the reference support resolves by.The price was disabled while the buyer was deciding.badges.simplex.chat/#/checkoutThese prices have changedStart again with the current pricesThe badge you chose was repriced while you were deciding.Nothing was charged.Start againprice_disabled / offer_disabled / offer_mismatchsimplex.chat/contactB5b. The catalog moved underneathPrices are checked at checkout and only there. A deprecated price is stillhonoured for someone mid-flow; a disabled one is refused. Repricing appends anew price rather than editing the old, so this is rare but reachable.Five checkout requests inside one minute, from one IP.badges.simplex.chat/#/checkoutToo many attemptsTry again in 46 secondsThe Pay button is disabled until then.Pay with Monero429, with Retry-After. The polling loop backs off tothat value rather than treating it as a failure.simplex.chat/contactB5c. Rate limitedFive checkout requests a minute per IP, because each one reaches a paymentprovider. Order polling gets sixty. The redemption path has no IP at all - itruns over SimpleX RPC and is throttled per signer instead.Continue in browsertier, months and method travel as URL parameters, so the site opens at B5the code is pasted into A5the only thing that crosses from the site back to the appHow to read thisthe normal pathwhat happens when nothing goes wronga variantan error, an empty state or a refusal of the screen on its lefta platform differencethe same question, answered differently by the store buildsbetween the two surfacesthe browser hand-off out, and the code back - nothing else crossesEvery screen sits one column to the right of the screen that leads to it, so a branch never doubles back on the reader. The cause of a branch is written above the screen it produces, in the colour of the line that reaches it. A caption sits under the screen itdescribes.What the app never doesCompute a priceevery figure comes from CRBadgeCatalog; the only arithmetic done locally is the crossed-outcomparison, which is never sentContact a payment providerthe app never sees Stripe or BTCPay. Its only peer is the badge service, over SimpleX, and onlyonce the user asks for a badgeStore the codenothing is written before the response arrives; a code consumed by a lost reply is recovered withcodes unredeemReuse a purchase keyevery redemption mints a fresh one, which is why a second code makes a second purchase ratherthan topping up the firstTake the service's word for a badgethe credential is verified against the issuer key the build already ships before anything iswritten or shownSay 'renews'prepaid months have no billing date, so A6 says Ends and never implies a renewalWhat a client build must be told, and what happens without itbadgeServiceAddressthe service's contact address, published by the operator at startup. Empty means the feature is off: A5 fails with a service error and A3's browser hand-offis hidden rather than broken.badgeWebBaseUrlthe checkout site. It must equal the service's [web] base_url, because Stripe's success_url is built from one end and the hand-off URL from the other; theymeet at the same ?order= page.badgePublicKeysthe issuer public keys, by index. A build without the index the service signs with rejects every credential it is issued, which looks to the user like aservice error and is not one.All three are ChatConfig fields with command-line overrides, which is how a client is pointed at a service run locally rather than the deployed one.The order behind B5 to B7invoicedpendingpaida payment arrivesthe webhook settles itexpiredfailedsettles late, which on chain is routineOnly paid is terminal. An invoice that expires or is refused can still settle afterwards, so every path back to paid stays open and the code is written then. No row holds both an order reference and a purchase reference: the two are joined only by deriving thecode from the order id, which is why an operator with the database alone cannot link a card payment to a profile.Redeeming, end to endthe appthe badge servicepurchaseBadge {code}signed with a purchase key minted for this redemption and never reusedverify, credit, debit, signone transaction, after the signaturethe checksum is verified first, so a mistyped code never costs a lookupbadgeCredential {credential, statement}the ledger rows travel with it, so both sides hold the same balanceThe app verifies the credential against the issuer public key it already shipsbefore it writes anything, so a service that signed with the wrong key cannotput a badge on a profile.Nothing is written before the response arrives. A code consumed by a lost replyis recovered with codes unredeem, never by reusing a stored key: everyredemption mints a fresh one.Not in this designSubscriptions and renewalsweb purchases are prepaid months; nothing recurs and nothing is cancelledStore purchaseremoved on iOS and Play until store evidence is verified, so a code bought in a browser is the only route to a badge thereTier upgradesa second code makes a second purchase and strands the first balance; it does not convert itLifetime and investor badgesthe ledger has no representation for an unbounded balance, so advance would lapse one every monthBadge alerts and Monday presentationthe reminder timer, the alert event and the +7 day recipient grace all wait for a later planAutomated crypto refundsBTCPay refunds are operator-initiated, with the codes tooling in OP1 and OP2The site's own idiomOne centred column, max-width 560and nothing outside it. The reference is the snrc pages, not the app: the site is a different surface and is drawn as one.Accent #0053D0, from website/tailwind.config.jsevery colour has a light definition on bare :root and a dark one under prefers-color-scheme, so neither theme is an afterthoughtOptions are bordered radio cardswith a 2px accent border when selected - the same visual language as PeriodCard in the app, so the two still read as one productReal fieldset, legend and label radiosone h1 per screen, visible :focus-visible rings, and a 150ms fade that prefers-reduced-motion turns offNo framework, no bundler, no CDNTypeScript compiled by tsc; dist/ is committed and CI fails when it drifts from src/; CSP default-src 'self' blocks nothing the page needsWhat the browser is never trusted withThe checkout request carries priceId, offerId and methodand nothing else. Badge type and months are derived server-side from those ids, so a tampered request cannot buy a Legend badge at a Supporter price.Every total on these screens is the server's own figurethe browser divides by 100 to print it, and computes only the comparison it strikes through, which is never sent anywhereorderId is 128 random bits and is equivalent to the codenever logged, never in a Referer, and deliberately absent from the Stripe cancel URL, which would otherwise leave a live capability in the history of someone whodid not payA deprecated price is honoured mid-flow; a disabled one is refusedand B5b is what that refusal looks like. The check happens at checkout and only there.Which URL shows which screen/B1the landing page, for anyone arriving cold#/tierB2question one#/monthsB3question two#/payB4, B4bquestion three, and the refusal when a provider is not configured#/checkoutB5, B5b, B5cthe summary, the repriced restart, and the throttle/?tier=&months=&pay=B5the app hand-off: three answers already given, so the summary is the first screen/?order=B6, B6b, B7, B7b, B5done URL for the whole post-payment life of an order, whatever its stateEvery one of these is a hash route inside a single document: there is no page load between screens, back and forward work through popstate, and a reload anywhere resumes from the URL alone.Design document. Each surface is one tree: the parent screen is always to the left of everything it leads to, normal paths in blue, variants in orange, a platform difference in grey, and the cause of every branch is written above the screen it produces.App screens are drawn in the iOS idiom; Android and desktop differ only in chrome. The site is drawn in its own idiom - one centred column at max-width 560, accent #0053D0 from website/tailwind.config.js - and is deliberately not a copy of the app's. Badge artwork is the shipped art, verbatim from MR/images/badge_{supporter,legend}.svg.One deliberate deviation from the plan: D2 specifies the SimpleX logo as the site's only image, but the tier cards here carry the badge art too, on the grounds that someone choosing between two badges should see them. Everything else follows D2 as written.Built today: the service schema, the pricing catalog, the ledger, code derivation and classification, credential signing, the RPC dispatcher and the operator CLI. Design, not yet built: every screen above, the web listener and its endpoints, both payment providers, and the client redeem path. Store purchase is removed on iOS and Play for the duration of this plan. \ No newline at end of file +Supporter badges - every screen, in the app and on the webone tree per surface, left to right - blue is the normal path, orange a variant, grey a platform difference, and the dashed pair is everything that crosses between the app and the siteplans/2026-08-21-badges-web-checkout.mdeditable SVGIn the appiOS idiom shown; Android and desktop differ only in chromeFor the operatorthe CLI behind the codesChoosing a badgeRedeeming a code, and what it gives youWhen something goes wrongSupport SimpleXSimpleX is built by people who believeprivate messaging should not depend onadvertising. A badge helps pay for it.Why SimpleX is built this wayChoose your levelRedeem badge codeA1. Where it startsBoth actions ship today. On iOS and Play, 'Redeem badge code' is nota secondary path - it is the only one, because store evidence is notverified and the store purchase was removed rather than leftcharging for nothing.Choose your level‹SupportYour levelBoth levels remove the file size limit.Legend raises it further.Supporter$7 / month2 GB filesLegend$70 / month5 GB filesPerks are app constants, not catalog data -the catalog carries prices only.ContinueA2. Choose your levelPrices come from CRBadgeCatalog, never the store products. Onesource, so the app and the site cannot disagree about what a badgecosts.Continue‹Your levelLegendPrepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%3 months is 2x the monthly price, 12 is 6x. Thesaving compares against the undiscounted totaland is never sent anywhere - the server pricesevery charge it makes.Continue in browserRedeem badge codeA3. How long - desktop and Android fossThe only builds with more than one way to pay, and the only onesthat link out. 'Continue in browser' carries the answers so far asURL parameters.Continue‹LegendHow to payCheckout happens in your browser.Nothing is charged in the app.CardVisa, MastercardBitcoinon-chainMoneroon-chainContinue in browserOpens badges.simplex.chat with tier, monthsand method already chosen.A4. Only where there is a choiceDesktop and Android foss only. The store builds have no methods atall and never reach this screen. The choice is a hint: the site asksagain if the parameter is unusable.The catalog fetch fails, or the operator disabled the price.‹SupportYour levelBoth levels remove the file size limit.Supporter—price unavailableLegend—price unavailableCould not reach the badge serviceinternalContinueDisabled, not hidden - a level that exists butcannot be priced still exists.A2b. The service is unreachableEvery option renders disabled and nothing crashes. The same statecovers a tier whose price was disabled by the operator - the appcannot tell the two apart, and does not need to.On iOS and Play there is nothing to link out to, so the screen endsat redemption.‹Your levelLegendPrepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%Buy a code at badges.simplex.chat from anybrowser, then redeem it here.Redeem badge codeno purchase button on this buildA3b. How long - iOS and PlaySame screen, different ending. The store purchase action is removedfor the whole of this plan, and no link out replaces it: Apple andGoogle reject steering to outside purchase for digital goods, so thescreen simply ends at redemption.Redeem badge code - and the only route on a store build‹SupportRedeem codePaste the code from your receipt.SXB-9K2M4-7QRT1-XZ5WPasteRedeemFormats as you type and folds I and L to 1,O to 0 - the same normalisation the servicedoes, so a code read off a screen cannot failon ambiguity alone.A5. RedeemingTwenty Crockford characters in five groups. The check character isverified before anything is sent, so a mistyped code never reachesthe service and never costs a throttle token.a valid code‹SettingsSupporter perksLegendshown on your profileENDS24 August 2027Prepaid months have no billing date. The badgeis reissued each month from the balance youalready paid for, and ends when it runs out.Add more monthsA6. After redeeming'Ends', never 'renews' - nothing recurs and the copy must not implyit does. The date is the paid-through date from the ledger balance,not the credential's expiry, which is a week-aligned internal theuser never sees.on the profile‹ChatsAliceAAliceLegendlast seen recentlyIN GROUPSAAliceLegendBBobSupporterCCarolThe badge travels with the profile, so it showswherever a profile does - in chats, in a memberlist, on a contact card.A7. How everyone else sees itThe point of the whole flow. The badge is a signed credentialcarried in the profile and verified by the recipient's own clientagainst the issuer key it already ships - not a flag the serverasserts, and not something a client can set for itself.Any error the service returns: code_invalid, code_used, code_expiredor rate_limited.‹SupportRedeem codeThis code isn't validcode_invalidThis code has already been usedcode_usedThis code has expiredcode_expiredToo many attempts. Try again in 4 minutes.rate_limited - retryAfter 240Could not verify the credentialinternalOne message per error the service can return.Anything else, including a credential that failsto verify locally, shows the service's own textrather than a blank screen.A5b. Every redemption errorA revoked code is deliberately indistinguishable from an unknown one- both say 'isn't valid', so a guesser learns nothing from arevocation. Support tells them apart with codes status; the wiredoes not.The last paid month has been issued and the balance is zero.‹SettingsSupporter perksLegendended 24 August 2027Your badge has endedThe months you paid for have all been issued.The badge stopped showing on your profile;nothing was cancelled and nothing is owed.Add more monthsA6b. The balance runs outAn exhausted balance is not an error: the service returns nocredential and a zero-balance statement, and the app says soplainly. Prepaid means it simply stops.simplex-badge-service$simplex-badge-service codes issue \--type legend --months 12 --count 3 \--batch conf-2026 --expires 2027-08-24SXB-9K2M4-7QRT1-XZ5WB-3NHD8SXB-2W7XP-4LMQ8-9DKR3-6TVZ5SXB-8HYNC-1FGJ6-5PBW2-7QSXD3 codes issued. Printed once; only hashes stored.OP1. Minting codesCompensation and promotional codes have no orderbehind them, so they are random rather thanderived. Printed once to stdout and never again -the database holds only SHA-256 hashes, which iswhat makes a stolen copy useless.support$simplex-badge-service codes status \--ref K7M2Qorder 8f3a...c21e paid 24 Augbadge legend, 12 monthscode redeemed 2 Sep--reveal refused: code already redeemedOP2. Resolving a referenceThe customer quotes the five-character referencefrom their receipt - never the order id, never thecode. --reveal is refused once a code is redeemedor revoked, on the same reasoning that stops theweb page disclosing it.On the webbadges.simplex.chat - one centred column, max-width 560, the logo the only imageBuying, start to finishThe other ways it endsRefused at checkoutbadges.simplex.chatSupport SimpleXSimpleX has no ads, no user accounts and nothing to sell.A supporter badge helps pay for the people who build it.Supporter$7 / month - 2 GB filesLegend$70 / month - 5 GB filesChoose your levelAlready bought a code?Redeem it in the app: Settings, Supporter perks.The badge shows on your profile. Nothing renews byitself, and no account is created.simplex.chat/contactB1. The landing pageNot in the plan, and it should be. D3 specifies four screens starting at thetier question, which assumes everyone arrives from the app. Someone reaching thesite from a link or a search needs to know what this is before being asked tochoose a level - and someone who already has a code needs telling thatredemption happens in the app, not here.Choose your levelbadges.simplex.chat/#/tierChoose your levelSupport SimpleX and lift the file size limit.Supporter$7 / month2 GB filesLegend$70 / month5 GB filesContinuesimplex.chat/contactB2. Question one: the levelThe wizard proper starts here. One centred column at max-width 560, generouswhitespace, accent #0053D0 - deliberately not the app's chrome, because it isnot the app. One question per screen, and a tier with no active price rendersdisabled rather than hidden.Continuebadges.simplex.chat/#/monthsHow long?Prepaid months. Nothing renews by itself.1 month$703 months$140save 33%12 months$420save 50%Continuesimplex.chat/contactB3. How longEvery figure comes from GET /api/catalog, the same totals the RPC catalog givesthe app. The browser formats minor units and computes the comparison only - itnever multiplies a price by a month count.Continuebadges.simplex.chat/#/payHow would you like to pay?Card is handled by Stripe. Bitcoin and Moneroare on-chain, through BTCPay.CardVisa, MastercardBitcoinon-chainMoneroon-chainContinuesimplex.chat/contactB4. MethodA method whose provider is not configured is refused at checkout withprovider_unavailable rather than hidden here, so a half-configured deploymentfails loudly instead of quietly offering less.Continuebadges.simplex.chat/?tier=legend&months=12&pay=xmrCheck your orderLevelLegendDuration12 monthsMethodMoneroTotal$420.00Pay with MoneroYou will be shown an address and a QR code.simplex.chat/contactB5. Where the app hand-off landsA user arriving from the app skips B1 to B3 entirely:?tier=legend&months=12&pay=xmr answers all three questions, so the summary isthe first thing they see. POST /api/checkout then carries priceId, offerId andmethod - never an amount, never a badge type. Both are derived server-side fromthe ids, so a tampered request cannot buy a Legend badge at a Supporter price.Pay with Monerobadges.simplex.chat/?order=8f3a...c21eSend 1.482 XMR$420.00 - this rate is held for 58:12MONERO ADDRESS48HqK2...9fRtWcCopyWaiting for the payment to confirmREFERENCEK7M2QBookmark this page - the address and the countdownboth live on this URL.simplex.chat/contactB6. Paying in cryptoPolls GET /api/order every 2s for the first minute then every 10s, and stopswhile the tab is hidden. A partial payment stays 'pending' - partials are thenormal first event of a multi-transaction payment, not a failure.it confirmsbadges.simplex.chat/?order=8f3a...c21e✓Paid. Here is your code.SXB-9K2M4-7QRT1-XZ5WB-3NHD8Copy codescan to carry it to your phoneREDEEM IT IN THE APPSimpleX -> Settings -> Supporter perks -> Redeem codeThis is the only place the code is shown.This page works until 23 September, and stops as soon asthe code is redeemed. The link is the code - treat it so.simplex.chat/contactB7. The code, onceDerived from the order id rather than stored, so a reload recomputes it whileonly its hash is ever at rest. Disclosure stops at redemption, at revocation, or30 days after settlement - whichever comes first.Redeemed, revoked, or thirty days after settlement.badges.simplex.chat/?order=8f3a...c21eThis code has been redeemedThe code is no longer shown hereIt was redeemed on 2 September. If that was not you,get in touch.Legend, 12 monthspaid 24 AugustThe order stays; the capability does not.simplex.chat/contactB7b. The page outlives the codeDisclosure ends at redemption, at revocation, or 30 days after settlement. Theorder is still here because support resolves a reference against it - but theURL stops being equivalent to the code, which is the whole point.The provider's section is missing from badge_service.ini.badges.simplex.chat/#/payHow would you like to pay?Monero is temporarily unavailableTry another method, or come back later.CardVisa, MastercardBitcoinon-chainMonerounavailableContinuesimplex.chat/contactB4b. A provider is not configured503 provider_unavailable from /api/checkout. The method is shown and disabledrather than omitted, so an operator who forgot a section sees it immediatelyinstead of wondering why nobody pays in Monero.Pay by card - Stripe returns herebadges.simplex.chat/?order=51c7...9d02Payment receivedWaiting for the card network to confirm.Still processingThis usually takes a few seconds. The page updates itself.If nothing happens for 15 minutes, we stop waitingand show you what to do next.simplex.chat/contactB5d. Coming back from the card flowStripe's success_url returns here with ?order=. The return is NOT proof ofpayment - only the webhook settles an order, so this screen polls rather thancelebrating. The cancel URL deliberately carries no order reference at all.The invoice expired with less than the full amount received.badges.simplex.chat/?order=8f3a...c21eThis invoice expired0.734 XMR arrived, which is not the full amountThe rate window has closed, so the shortfall is no longermeaningful. Quote the reference below and we will sort it out.REFERENCEK7M2QStart a new invoiceNever shows the code, and never the ?order= URL.simplex.chat/contactB6b. Expired, and underpaidThe one state that needs a human. An expired-but-paid invoice still settleslater - late on-chain settlement is routine and moves the order to paid - but anunderpaid one stops here with the reference support resolves by.The price was disabled while the buyer was deciding.badges.simplex.chat/#/checkoutThese prices have changedStart again with the current pricesThe badge you chose was repriced while you were deciding.Nothing was charged.Start againprice_disabled / offer_disabled / offer_mismatchsimplex.chat/contactB5b. The catalog moved underneathPrices are checked at checkout and only there. A deprecated price is stillhonoured for someone mid-flow; a disabled one is refused. Repricing appends anew price rather than editing the old, so this is rare but reachable.Five checkout requests inside one minute, from one IP.badges.simplex.chat/#/checkoutToo many attemptsTry again in 46 secondsThe Pay button is disabled until then.Pay with Monero429, with Retry-After. The polling loop backs off tothat value rather than treating it as a failure.simplex.chat/contactB5c. Rate limitedFive checkout requests a minute per IP, because each one reaches a paymentprovider. Order polling gets sixty. The redemption path has no IP at all - itruns over SimpleX RPC and is throttled per signer instead.Continue in browsertier, months and method as URL parametersthe code is pasted into A5the only thing that crosses from the site back to the appDesign document. Each surface is one tree read left to right: a screen is always to the left of everything it leads to, each lane is labelled for what its screens have in common, and the cause of a branch is written above the screen it produces, in the colour of the line that reaches it.App screens are drawn in the iOS idiom; Android and desktop differ only in chrome. The site is drawn in its own idiom - one centred column at max-width 560, accent #0053D0 from website/tailwind.config.js - and is deliberately not a copy of the app's. Badge artwork is the shipped art, verbatim from MR/images/badge_{supporter,legend}.svg; D2 asks for the logo alone, and the tier cards carrying the badges are the one deliberate deviation.Built today: the service schema, the pricing catalog, the ledger, code derivation and classification, credential signing, the RPC dispatcher and the operator CLI. Design, not yet built: every screen above, the web listener and its endpoints, both payment providers, and the client redeem path. Store purchase is removed on iOS and Play for the duration of this plan. \ No newline at end of file