Merged
Conversation
#2647 fixed this defect at useZeroDev.ts:222 but missed its sibling here, so half the campaign failures are still undiagnosable in production. Sentry's console integration serializes each console argument. Passing an object holding an array of claims lands in the issue as the literal "[object Object]" — so the two facts the warning exists to carry, which campaign and why, were both unreadable. PEANUT-UI-SJC is a live example from today; the registration-side sibling now reads "utm:pix=unknown" instead. Pre-join the pairs into one string, exactly as useZeroDev.ts does. The message stays constant so Sentry keeps grouping these into a single issue rather than opening one per campaign combination. This matters right now: three live marketing tags (brazil, pix, faster-payments) currently resolve to no campaign, and the invite page was the surface that could not say which. Test pins the payload is a string, names both campaigns and reasons, and pins the message constant. Verified non-vacuous: reverting to the object payload fails it, restoring the join passes.
Ask our most active users for a 15-min founder call (TASK-21189). The cohort lives in the PostHog flag user-interviews-invite (release condition on username), never in code - list edits and the kill switch are one click in PostHog and no usernames ship in the bundle. The card leads the carousel for targeted users, opens the shared cal.com event via the Capacitor-safe helper, and inherits the standard 7-day X-dismissal. Temporary campaign: delete the card, flag, and i18n keys when it ends.
…n-payload fix(badges): make the invite-page campaign warning readable in Sentry
…rdown checklist Applied from the automated review pass: (1) await openExternalUrl so a native Browser.open failure surfaces in CarouselCTA's onClick catch instead of a detached rejection; (2) the analytics comment no longer claims flag exposures equal impressions (they fire per evaluation, prod-only, dismissed users included) and points readers at cal.com bookings + a $host filter; (3) the delete-together checklist now names the dev/home-ctas preview entry so teardown removes all three copies.
The committed aleks-n-hugo/15min event 404s (CodeRabbit catch, verified). Replaced with the hugo0+abalinda dynamic group link, which resolves and books both hosts' mutual availability.
Aleks isn't a founder; only Hugo is. All four locales, the dev preview, and the code comments now say team.
feat(home): flag-gated user-interview invite card in the carousel
The shared icon container is size-8, and the Tailwind preflight img max-width clamps any larger logo back to 32px - so the size bump rides the existing iconContainerClassName prop (tailwind-merge lets size-11 override size-8). The asset already animates: it is the animated webp, which Next serves untouched (ANIM chunk verified through /_next/image).
ui: render the waving mascot at 44px (follow-up to #2654)
…end copies The card-vs-Peanut markup was modeled twice in the frontend — once as a server action, once copied into the merchant page with a header saying to delete it when #2108 landed. #2108 landed. Both computed their own dolarapi call and their own issuer fee, and neither could agree with the rate on screen. The hook keeps its signature and its never-fails contract: a backend outage falls back to the same static table as before, so a frontend deploy that precedes the backend degrades to today's numbers instead of blanking a surface.
…compute Also splits the two failure kinds: a 404 is the backend proving there is no comparison to publish, so the row is hidden rather than filled with the static assumption, which exists for an unreachable backend.
Review found the distinction collapsing at four points, each publishing a saving claim on evidence there is none: - a well-formed zero markup threw, so the hook fell back to the static 9.13% - an out-of-band locked-price recompute silently returned the market markup, still tagged live — the exact failure the recompute exists to prevent - the merchant page and the post-card-spend nudge both turned the hook's null into the static table, striking through every menu price by 9.13% fetchCardMarkup now returns null for 'the backend published no comparison' and throws only when the response could not be obtained or trusted. Also bounds the live observation age client-side, formats the CompareSavings date in UTC (a hydration mismatch on statically generated pages for any viewer west of Greenwich), rejects a negative percent instead of publishing it as a range, and formats amounts in the page locale.
…0811-100504 content: publish latest to production (src/content → peanut-content@062cdba)
…dates
Both CodeRabbit findings are the same defect class as the last round — a
literal check that misses an equivalent representation and falls through to the
static claim.
The wire pattern admits "0.0" and "0.00", not just "0". Matching the text
sent those down the invalid path, so a backend saying 'no gap to show' produced
a 9.13% saving claim. The check now compares the parsed value.
new Date('2026-02-30') is 2 March, so a typo in a hand-authored MDX date would
publish a verification date that never happened. verifiedAt is now round-tripped
against an exact YYYY-MM-DD.
…0811-102159 content: publish latest to production (src/content → peanut-content@c708cdd)
…0811-103319 content: publish latest to production (src/content → peanut-content@5954b68)
feat(fx): consume the shared card-markup endpoint, delete both frontend copies
…0811-110227 content: publish latest to production (src/content → peanut-content@2a1c593)
…ient-side The token was computed in the browser as SHA-256 of a salt that ships in the client bundle plus the userId, both public. Anyone could reproduce any user's token and open their support conversation to read it and post as them. Fetch it from the new authenticated GET /user/crisp-token instead, which derives it server-side with a secret the browser never sees and issues it only to the authenticated caller. Same undefined-until-resolved contract and cache, so SupportDrawer's token gate is unchanged. Requires the peanut-api-ts endpoint to be deployed first.
…sient failures Address code review of the client hook: - Reset the token to the current user's cached value (or undefined) before fetching, so an account switch never serves the previous user's token while the new one loads (cross-user exposure window). - Verify the userId the route echoes matches the account we fetch for, so a stale auth bearer can't bind the widget to another user. - Retry the fetch a few times, so a transient blip doesn't strand the support drawer's loading gate for the whole session.
Companion to peanut-api-ts#1326, which deletes GET /points/leaderboard and GET /points/time-leaderboard — the unauthenticated ranked roster the 2026-08-10 probing campaign used as its targeting oracle. This removes the only consumer (the /dev/leaderboard page), the service method, the demo mock, and the leaderboardRank field no component ever rendered.
Found by code review — the untyped literal kept advertising a field the API stopped sending.
…derboard chore: delete the points leaderboard consumers (postmortem F2)
Anthropomorphic açaí bowl sticker, matching Peanut's badge house style. Paired with the ACAI_POWERED backend catalog entry (waitlist skip, capped campaign, expires 2026-08-17).
…r-side fix: fetch Crisp support token from the API instead of deriving it client-side
…essage handshake The crisp-proxy iframe URL carried email, legal name, userId, wallet and provider links, plus the Crisp session token (a bearer credential after the server-side token change). A query string leaks into Vercel logs, browser history, Referer headers, and the $current_url of every analytics event fired from the iframe — confirmed stored in PostHog. Postmortem F5, TASK-21353. The proxy now asks its parent for the init payload (CRISP_PROXY_REQUEST_INIT) and boots Crisp only when the reply lands, so the parent can never post before the iframe listens — same reliability the URL transport was built for, with nothing identifying in the URL.
…0812-122901 content: publish latest to production (src/content → peanut-content@ea76eea)
…0812-123904 content: publish latest to production (src/content → peanut-content@4a153e2)
fix(qr): recognize scanned Solana and Tron addresses
…selects both hosts and 15 min
…l-link fix(home): correct user-interview cal link
Backend catalog gained SURF_UP for the summer 2026 campaign; this is the UI half of the asset contract — artwork, regenerated manifest, and the bespoke first-person share copy.
The ENS badge is earned by paying at an ENS name, so the art has to be in
public/ before the API can award it: badge.utils.test.ts asserts every path
the backend catalog declares resolves to a real file, and the API's
check-badge-catalog.ts refuses a manifest that drifts from the catalog. Ship
the UI half first and merge order stops mattering.
iconUrl is not stated in the catalog entry — it falls out of the
/badges/${code.toLowerCase()}.svg default — so the manifest line is generated,
not chosen here.
Name and description stay backend-owned. The share line is the one piece of
badge copy the frontend owns, because it is the user speaking on their own
share card, not the catalog describing the badge in third person.
…n be earned The API half of the ENS badge has been live and inert. It awards at settlement from a name recorded on the charge intent, and we never recorded one: the app resolves ENS in the browser and posts the resolved address, so by the time a charge exists the name the payer typed is gone. Nothing on the server could tell `/vitalik.eth` apart from `/0xd8dA…`. So the charge now carries `requestProps.recipientEnsName`. The server treats it as a hint, not evidence — it re-resolves the name at payment completion and requires it to still point at the address the payment actually paid — which is why sending it is safe, and why sending the *right* one is what matters. The filter lives in createCharge rather than at each call site, so no caller has to remember the rule: pass the raw identifier, and only a real name survives. A name has a dot (`vitalik.eth`, `alice.peanut.me` — a Peanut name is an ENS name); a bare handle `alice` and a `0x…` address do not, and are dropped. The predicate is the existing validateEnsName/ENS_NAME_REGEX, not a new one. Two flows carry a recipient the payer actually typed, and both now pass it: the semantic-request flow (`/vitalik.eth`), and the crypto withdraw, where withdrawing to a name is still paying at one — guarded there on the name still matching the address being paid, because that input re-resolves per destination chain and a stale pairing would describe a different address. Direct send resolves a Peanut username to a wallet and has no name to send; contribute-pot takes its recipient from the stored request, not from the payer; requestByUsername's recipient is the requester's own address. None of them get the field. api.openapi.json is a captured snapshot and the API's own copy has not been refreshed for this field, so the three /charges request bodies were patched by hand to mirror the deployed TypeBox schema; api.generated.ts is regenerated from it with gen:api, unedited.
The badge is earned by paying at a name AND by being paid at yours, but the line only described the payer, so half of everyone who shared it was saying something they had not done.
The check existed so a stale name never reached a server that would re-resolve it; the server no longer re-resolves anything, and `recipientEnsName` is read for one purpose — awarding a badge that grants nothing. `validateEnsName` stays as the single filter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The /shhhhh landing page showed 3 of the 12 badges that skip the card queue. The API's badge registry (SKIP_BADGE_CODES) is the source of truth; the page now shows all of them. NITA is labeled 'Nita' because 'Nita's Recommendation' clips on the 375px tile.
Hand-copied /badges/* paths duplicate badge-assets.json and drift when art is re-cut. getBadgeIcon() is the accessor every other badge surface uses; it also gives fallback art for free.
Twelve tiles made door 01 four rows tall against door 02's two paragraphs. Collapsed shows the original 3 with '+9 more'; door 02 no longer stretches to match door 01's height (md:self-start).
fix(shhhhh): list the full waitlist-skip badge set
Art for the TRON catalog entry in the sibling API branch: the Tron mark drawn as the kawaii character, matching how the Arbitrum badge treats its own chain logo. Manifest regenerated from the API catalog rather than hand-edited.
The first pass invented the logo from memory and produced a triangle with a letter T inside it, which is not the Tron mark. Regenerated against src/assets/chains/tron.svg, our own Tron chain icon: a three-sided pyramid seen from above, three facets radiating from an off-centre junction, apex down, notch on the upper-right edge.
The wireframe reads best but the navy coin swallowed it: black pupils on dark navy, seen through hollow facets, are invisible at the ~64px grid size the badge list actually uses. Coin is now flat yellow and the eyes are sized to fill the large left facet.
Owner pick out of the six light-coin wireframes. Copy is unchanged, so the API catalog entry needs no edit.
…anager The recipientEnsName doc promised a server-side re-resolve that peanut-api-ts 2f570b02 deleted — validateEnsName at the call sites is the only filter left, so the comment now says so. The withdraw page also validated the raw input while useChargeManager validates trim().toLowerCase(); a padded name failed validation there and silently earned no badge. Both call paths now normalize the same way.
feat(badges): add Surf's Up artwork and share line
…0817-113843 content: publish latest to production (src/content → peanut-content@9101046)
The Peanut Split glove-hand mark as a house-style sticker (badge-draft pipeline, generated against the real logo, deliberately no face). Asset manifest regenerated from the api catalog; pairs with the peanut-api-ts SPLITTER registry PR.
Restore the badges-recraft output framing (viewBox 0 0 2048 2048, 1024px, preserveAspectRatio none) instead of the hand-cropped square — the tight crop rendered Splitter oversized next to sibling badges in grids. Only the brand pink is snapped to #FF90E8.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
feat(badges): Splitter artwork + share line
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch Sync
main is 63 commits ahead of dev.
Commits to sync:
• fix(badges): make the invite-page campaign warning readable in Sentry
• feat(home): flag-gated user-interview invite card in the carousel
• Merge pull request #2651 from peanutprotocol/fix/invites-campaign-warn-payload
• review: await cal.com open, honest impression-proxy comment, full teardown checklist
• review: booking link is the live cal.com dynamic group URL
• copy: the call is with the team, not the founders
• Merge pull request #2654 from peanutprotocol/hotfix-user-interview-cta
• ui: render the waving mascot at 44px
• Merge pull request #2655 from peanutprotocol/hotfix-user-interview-cta
• feat(fx): consume the shared card-markup endpoint and drop both frontend copies
• fix(fx): mirror the corrected issuer-fee model in the locked-price recompute
• fix(fx): keep 'no comparison' and 'could not reach the model' apart
• Update content submodule to latest main
• Merge pull request #2659 from peanutprotocol/auto/update-content-20260811-100504
• fix(fx): accept every spelling of zero, reject overflow verification dates
• Update content submodule to latest main
• Merge pull request #2660 from peanutprotocol/auto/update-content-20260811-102159
• Update content submodule to latest main
• Merge pull request #2661 from peanutprotocol/auto/update-content-20260811-103319
• Merge pull request #2658 from peanutprotocol/feat/fx-card-markup-ui
• Update content submodule to latest main
• Merge pull request #2664 from peanutprotocol/auto/update-content-20260811-110227
• fix: fetch Crisp support token from the API instead of deriving it client-side
• fix: reset crisp token on account switch, verify identity, retry transient failures
• chore: delete the points leaderboard consumers (postmortem F2)
• chore: drop stale leaderboardRank from the e2e tier-info mock
• Merge pull request #2667 from peanutprotocol/hotfix/delete-points-leaderboard
• feat: add ACAI_POWERED badge artwork for Brazil creator campaign
• Merge pull request #2666 from peanutprotocol/hotfix/crisp-token-server-side
• fix: move Crisp proxy user data and token out of the URL into a postMessage handshake
• review: push payload updates over the channel, reuse setCrispUserData, harden handshake
• review: reply only to the mounted proxy iframe, reset status on token/locale remount
• Merge remote-tracking branch 'origin/main' into fix/crisp-proxy-pii-in-url
• review: never re-apply an unchanged prefill on live updates
• Merge pull request #2672 from peanutprotocol/feat/acai-powered-badge
• Merge pull request #2669 from peanutprotocol/fix/crisp-proxy-pii-in-url
• fix(qr): recognize scanned Solana and Tron addresses
• fix(qr): retry lowercased only for all-uppercase scans
• chore(qr): tighten the case-handling comment
• Update content submodule to latest main
• Merge pull request #2677 from peanutprotocol/auto/update-content-20260812-122901
• Update content submodule to latest main
• Merge pull request #2679 from peanutprotocol/auto/update-content-20260812-123904
• Merge pull request #2675 from peanutprotocol/fix/qr-scanner-base58-case
• fix(home): point user-interview card at the dynamic cal link that preselects both hosts and 15 min
• Merge pull request #2683 from peanutprotocol/hotfix-user-interview-cal-link
• feat(badges): add Surf's Up artwork and share line
• feat(badges): add ENS artwork and share line
• feat(badges): send the typed ENS name with the charge so the badge can be earned
• fix(badges): make the ENS share line true for both sides of the award
• refactor(badges): drop the withdraw ENS pairing check
• fix(shhhhh): list the full waitlist-skip badge set
• refactor(shhhhh): derive badge icons from the generated asset manifest
• fix(shhhhh): collapse the badge grid to 3 + a 'more' toggle
• Merge pull request #2698 from peanutprotocol/hotfix/shhhhh-badge-list
• feat(badges): add Tron Native artwork and share line
• fix(badges): draw the actual Tron mark, not a T in a triangle
• fix(badges): light coin and a bigger face so the eyes are legible
• feat(badges): pick the winking Tron mark from the matrix
• fix(ens): true trust comment; withdraw trims the name like useChargeManager
• Merge pull request #2691 from peanutprotocol/feat/surf-up-badge
• Update content submodule to latest main
• Merge pull request #2699 from peanutprotocol/auto/update-content-20260817-113843
Auto-generated by Chip 🐿️