Senior / Lead · Content Design
Copy as a governed system, not a queue of tickets.
Good UX writing cleans up after design. Great UX writing shapes it before production.
I build the frameworks, pattern libraries, and tooling that put it there — voice consistent and dark patterns out, across iOS, Android, and web.
Flagship · System
Copy governance systemA closed loop from Figma to a living canon — two tools with a self-updating source of truth between them.
The problem
The product ships copy across three platforms from one Figma source that doubles as dev handoff. At ~1,600 strings, three things kept breaking:
- Drift. The same string worded differently across platforms; regressions reintroduced in new versions.
- Canon rot. The voice & tone guide lived in Notion as a doc nobody re-read; review decisions never made it back in.
- Bottleneck. Every “is this on-brand?” routed through one writer. Copy QA didn’t scale past one person.
What I built
Deterministic · scale
copy-parityNode.js · Figma REST API
extract→
pair→
lint→
report→
comment
- Pulls every text node, pairs equivalents across platforms, lints each against
rules.yaml — the copy canon as code, where adding a rule means one regex in a config file.
- Posts findings back onto Figma frames as node-anchored comments. Read + comment only — never mutates the canvas.
- Catches the machine-checkable class across 1,600+ strings in one pass: casing, broken tokens, drift, banned constructions.
Judgment · on-demand
ux-copy-auditAI skill · Notion via MCP
screenshot in→
check canon→
before/after out
- Anyone — PM, designer, writer — drops a screenshot or Figma frame. No briefing; the skill carries the context.
- Reads the live authority stack from Notion: the UX Copy Patterns DB (
Status = Active), tone-of-voice pillars, naming rules — then judges the screen against canon.
- Returns a before/after with rationale. Approved calls graduate back into the Patterns DB and land in Field Notes with per-writer attribution.
How the loop closes
1Figma / screenshotany surface
→
2Auditregex + judgment
→
3Before / after+ rationale
→
4Human approvalwriter / PM
→
5Graduate to canonPatterns DB
↩ feedback
Every approved decision sharpens the canon — so the next audit starts smarter. The canon lives as the system’s runtime state, kept current by every audit. And it’s team-scoped: multiple writers feed and draw from the same source.
Impact
1,600+
copy changes triaged into a Notion review pipeline
Team
standard for UX-copy audit — writer capacity removed as the bottleneck
30+
high-stakes surfaces rewritten — paywall family, error taxonomy, onboarding, the chatbot funnel
I treat copy as an owned system — methodology, canon-as-code, tooling, and a feedback loop that keeps voice consistent at team scale.
Next: merge the deterministic and judgment halves into one LLM-in-the-loop linter.
A slice of the parity matrix — what pair + lint emit
Caught as a pattern, fixed once
The payoff, in one number — an unverifiable “most accurate” accuracy claim had quietly shipped across the product:
81screens carried it
6surfaces
1ruling in the canon
Chat flows33
Elite Hub17
Audio Calls16
Membership9
Catalogue4
Chat Next2
One claim, shipped to 81 screens. Plain text-similarity linting misses this class — it isn’t visible “drift,” it’s a canon issue. The system surfaced every instance; one ruling fixed them all.
Appendix · Audit log
What was live in productionBefore the audit system existed — now caught automatically. The pile the linter clears so I can spend judgment on the hard calls.
− Process message+ Incorrect email or password. Please try again.
Placeholder text shipped as the live login error.
− You need >30 credits on the balance+ You need more than 30 credits in your balance
A math symbol in reader-facing paywall copy.
− Need an account? Sign up+ Already have an account? Log in
Backwards — the link sat on the Sign Up screen itself.
− Recomended for You+ Recommended for you
Shipped identically on iOS + Android v1.
− See psychic+ See psychics
A v2 regression — a singular CTA led to a list of many.
− You can archive unnecessary chats!+ You can archive chats you don't need. Swipe left to try it.
Calling a user’s own chats “unnecessary” works against an empathetic voice.
Case study · Trust
Trust by designResonance without manipulation — in a product built on spiritual hope, for people who arrive at vulnerable moments.
People come to a psychic-chat product mid-breakup, in grief, or facing a hard decision — and the business monetizes that moment. The frictionless copy manipulates: a guaranteed soulmate, a countdown, a struck-through price. I held the line between resonance and manipulation on every surface a user meets — the conversation, the promos, and the money flow — and encoded it into the system so the line holds without me.
Part 1 · In the conversation
Auditing a chatbot intake funnel where the whole conversation is the unit of analysis
The brief was “here’s the flow, fix the copy” — a ~220-string chatbot funnel steering new users toward a trial psychic chat. I reframed the problem before touching a word.
A line can be perfect alone and broken in sequence — “Got it.” landing four times in a row. So the unit of audit isn’t the string. It’s the assembled dialogue.
Path-level finding · assembled transcript
answers gender→Got it.
answers status→Got it.
taps “Other”→Got it.
answers focus→Got it — this is an internal journey.
Each line is fine on its own. Four “Got it” openers in a row read like a machine — a rhythm only visible once you assemble the path. You only catch it by reading the assembled path. So the fix below changes what each reaction is doing, screen by screen.
I ran two passes: a string-level pass, and a path-level pass — programmatically assembling seven end-to-end transcripts and reading them as real conversations for rhythm, repetition, and cumulative tone. The audit framework had no conversational lenses, so I built five and added them as a reusable pattern family:
PersonaTurn designSequence rhythmPromise vs. intentBot → human handoff
−One of the most common and most painful fears. You're not alone in it.
+So many people secretly carry "am I enough" — you're not alone in it.
Vulnerable-moment reaction. “Common” quietly devalues the very fear the user just admitted. Normalize without diminishing.
−Your psychic will show you where closure actually comes from.
+Waiting for closure that never came — that ache is real. It deserves a real answer.
Outcome guarantee → process. Process verbs (read, tune into, look at) are fine; guaranteeing an outcome is a hard voice prohibition — and a compliance risk for an entertainment-only service.
−The future feels uncertain right now. I want to understand why.
POV bug (flagged 🔴). The bot speaks in the user’s own voice, then answers itself. Persona integrity: the assistant prepares the question, the human psychic reads.
The lead moment — give each line a job
After the PM signed off, the real question surfaced: how are these responses even formed? The whole corpus ran on one generator — mirror the user, weigh the feeling, bridge to the psychic — so the whole thing read like a catalog of captions. The assistant never responded — it evaluated. The fix wasn’t another polish pass. It was giving authors a palette of conversational jobs and a quota per path.
User taps: “I work hard but I’m invisible and overlooked”
MirrorBeing good at what you do and still not being seen — that chips away at you.
PresenceThat’s an exhausting way to work. I’m glad you said it straight.
NormalizeMore people carry this one than ever say it out loud.
Silence · bridgeNoted — word for word. This goes to your psychic as is.
One option, four different jobs. The rule: on a ~7-reaction path — 3 mirrors · 2 presence · 1 normalize · 1 bridge. A constitution for the copy — the rules authors write to.
44
changes shipped (13 critical / 31 refinements) across the funnel
7
end-to-end transcripts assembled to audit at the path level
0 → 1
“human” signal at the bot → person handoff (present in none of 232 replies)
Part 2 · On the promo surfaces
The same line, held across quiz, horoscope, and tarot promos
The conversational work set the principle; these show it applied at product breadth — keep the emotional pull, remove the certainty the product can’t honestly claim.
1
Quiz — result screen
1
−There’s a high chance that you’ve already met your soulmate from a past life. Those intense feelings you have are real soul nudges, not just daydreams.
+Some people describe feelings like this when they’ve crossed paths with someone important before.
Quiz result. A past-life soulmate reunion stated as fact → an honest observation. The hook survives; the claim doesn’t.
1
2
Tarot — daily insight
1
−…This card confirms that you are on the right path… Your wishes have been heard.
+This card is an encouraging sign — keep moving forward with confidence.
Guaranteed outcome. “Confirms” and “wishes have been heard” are destiny language — a hard prohibition. Encouragement without the guarantee.
2
−Get the best guidance and advice! …you can be sure to find it in your Tarot Reading.
+Want to go deeper? Chat with a psychic for guidance tailored to you.
Superlative + guarantee. “The best” and “you can be sure to find it” → an honest CTA. A shared template repeated across several tarot screens.
Same principle · other surfaces
−✨ In 8 days, you won’t feel alone anymore. You’ll know someone special is just around the corner…
+After these 8 days, you’ll have a clearer sense of what you’re looking for — and the signs to recognize it when it shows up.
Interval reading. A manufactured emotional guarantee (“you won’t feel alone”) → a promise the product can actually keep.
Part 3 · In the money flow
Dark patterns, taken out of the paywall
In a money flow, the frictionless copy manipulates the choice. I took three patterns out — two the governance system now catches on its own, and one that needed a person to name.
−I don’t want free week of usage — I want to cancel my subscription
+Cancel subscription
Confirmshaming, on the cancel button. The user had to narrate refusing a free week just to quit. Caught by the confirmshaming rule; the free-week offer stays as a separate button, minus the guilt.
−Don’t miss out on a single perk!
+See everything your membership includes
Manufactured FOMO. “Don’t miss out” invents urgency where there’s no deadline. Caught by the urgency-scarcity rule → a plain, informative label.
Encoded once, enforced everywhere
Neither was spotted one screen at a time — confirmshaming and urgency-scarcity are rules I encoded in rules.yaml, so the linter flags every instance on every screen automatically.
The one no rule could catch
Some risks aren’t pattern-matchable — they need a person who knows the law.
Fictitious former price · 16 CFR §233.1
A struck-through “original” manufactures a discount — but there was no record of that higher price ever being the standing one. I flagged it as an FTC compliance risk and escalated it to PM and legal — the kind of judgment a linter can’t make. And I set Crowdin as the localization source of truth, so fixes propagate instead of drifting.
Illustration of the flagged pattern — not the product’s real pricing.
One line runs through all of it — the conversation, the promos, the paywall: a product built on hope can’t grow by spending users’ trust, and the honest choice should never be the harder one.