Skip to main content
02Fintech · fraud review console2026Two weeks

Verdict

Method demo · working prototype

Working prototypes, not screenshots. Try A, D and E on a case, then Z to undo.

A fraud reviewer decides in 45 seconds with a model whispering a number. The interface has to make that number arguable.

Client

Fintech · fraud review console

Brief from

Myself, with a two-week clock

Team

Solo

Users

None. Domain researched, not observed.

Role

Product design, design system, front-end

Surface

Internal ops console, desktop, dark-first

Duration

2026 · Two weeks

In short

  • Self-initiated concept. Researched from published card-network and risk-ops material, not lived. No users, no deployment, no outcome claims.
  • Built as a working prototype: three screens, two flows, sixteen components, all keyboard operable.
  • 287 design tokens across three enforced tiers, with 68 contrast pairs measured by the build and zero below their WCAG gate.
  • Exists to answer one question a hiring manager cannot otherwise check: is this person's work considered, or is it generated?
Verdict, The score instrument: the model's estimate, its confidence span, the live policy threshold and four ordinal risk bands on one recessed scale.

What it moved

The numbers, in plain sight.

287

Design tokens, three tiers

68

Contrast pairs measured, 0 below gate

16

Components, five categories

3.09:1

Contrast floor, both themes

The story · 11 chapters

How Verdict got built.

rev 00brief

What this is, and what it is not

Card processors flag a fraction of transactions for human review. A model scores each one, and a person approves or declines. Verdict is the console that person works in.

It is self-initiated. There is no client, nobody was interviewed, and nothing is deployed to production. I researched the domain from published card-network documentation and risk-operations writing. I have not worked in it, and the case would be worth less if I pretended otherwise.

So it claims no outcomes. What it claims instead is that every number below is a property of the repository, reproducible by anyone who clones it and runs the build.

A concept has no users. It can still have receipts.

rev 01investigation

A shift on the queue

Two people use this. A reviewer works 200 to 400 flagged transactions a shift, measured on both speed and accuracy, which pull against each other. A risk manager sets the threshold that decides how much lands in the queue at all.

Their problems are different in kind. The reviewer is drowning in context switches. The manager is flying blind.

Reviewer

Reassembly, not decision

Evidence sits across four tabs. Rebuilding context costs more than the judgement does, on every single case.

Reviewer

A number with no argument

The score arrives bare. Analysts either rubber-stamp it or ignore it, and both of those are failures.

Reviewer

Hesitation

No undo, so people slow down to be sure. Caution looks like diligence and costs throughput.

Reviewer

Silence after the fact

A wrong decline loses a good customer quietly. Reviewers rarely learn whether they were right.

Manager

Moving a line blind

The pain that earned its own screen

Nothing says what a five-point change costs in false positives, queue volume, or analyst hours.

Both

No stated reason

Cases arrive with no explanation of why. Not every case is here because of the score, and the tool never says which.

rev 02insight

Risk is ordinal, so the traffic light is a bug

Almost every tool in this space renders risk as red, amber and green. That is a categorical encoding of an ordered quantity. It collapses a continuous score into three buckets, it fails colour-blind reviewers, and it dies on the projector in a shared operations room.

Verdict encodes risk as one hue climbing in lightness, plus an explicit rank, plus the printed numeric range. Three redundant carriers, so the ordering reads with no colour perception at all.

This is the decision the rest of the system hangs off. It produced the token architecture, the accessibility argument and the central instrument in one move.

Why the traffic light is a bug

Traffic light

what almost every tool ships

 
 
 
 

desaturated

 
 
 
 

Four ordered levels forced into three categories, and two of them are the same colour. Desaturated, the order is gone entirely.

Ordinal ramp, rank, range

what shipped

R1 Low0–39
R2 Moderate40–64
R3 Elevated65–84
R4 Severe85–100

desaturated

R1 Low0–39
R2 Moderate40–64
R3 Elevated65–84
R4 Severe85–100

One hue climbing in lightness, so the order survives desaturation. The rank and the range survive losing colour altogether.

Both encodings, in colour and desaturated. The traffic light loses its ordering the moment colour goes; the ramp does not, and the rank survives even that.

rev 03decision

How the work actually ran

Six screens generated in an afternoon, five of them killed by the end of it. That is not waste, it is the method.

Slop is what happens when the generated artifact becomes the deliverable. Here the artifact is disposable by design: it exists to make an argument visible fast enough to reject. Nothing generated survives contact with the system layer, because the system layer is built after the exploration and rebuilt from tokens up.

The order matters more than the tools. Tokens before components, components before screens, screens before flows. Reverse any two of those and the result is a demo that cannot be extended.

How the work actually ran

01

Artifacts

Claude artifacts

Six whole screens in an afternoon

Five of them, on purpose

02

Tokens

TypeScript source, generated CSS

Three enforced tiers, a contrast gate

Every colour the artifacts invented

03

Components

Radix, CVA, Storybook

Sixteen components in five categories

The artifact markup entirely

04

Screens and flows

Next, real state, real keyboard

Three screens, two flows, one system

Nothing. This is what ships.

What each stage produced, and what it threw away. Only the last stage keeps its output.

rev 04decision

Where the model gets a vote

Speed is only worth having if judgement stays where it belongs. So the boundary is explicit rather than intuitive.

The clearest way to show that boundary is not to describe it. It is to show what the model proposed and what shipped instead.

Where the model gets a vote

The model decides

I take the output as-is

  • Layout permutations to look at
  • First-draft copy for every string
  • Boilerplate variants and prop plumbing
  • Mock data volume and edge-case shapes

The model proposes, I decide

Its answer is an option, not a result

  • Information hierarchy on a screen
  • Which states a component must carry
  • Naming, in the user's vocabulary
  • Where a pattern is reused or forked

I decide alone

Never delegated, not once

  • Token architecture and the tier rules
  • The accessibility contract
  • What the flow is, and where friction goes
  • What the interface refuses to do
Three columns, and the third one never moved during the build.
 The model proposedWhat shipped, and why
Risk displayRed, amber and green severity chipsOne ordinal ramp with rank and range. Colour alone fails 1.4.1 and dies on a projector.
Destructive actionA confirm dialog on every declineOptimistic commit with a six-second undo. A confirm seen 300 times a shift trains dismissal.
The scoreA bare 87 in large type87 with its confidence span and its contributing signals. A bare number is obeyed or ignored.
EvidenceOnly the signals that raised the scoreSignals both ways, same weight and size. Showing one side turns a decision aid into a prosecution.
Queue orderSorted by score, riskiest firstSorted by time left. A case nobody decided is the one outcome you cannot recover.
Bulk actionsOne confirm dialog for every bulk actionApprove commits with undo, decline asks first. The consequences are not symmetrical.
Why am I hereNothing. The score was assumed to be the reasonEvery case states why it is in the queue. Three of nine are held by a rule, not the model.

rev 05decision

Tokens before components, and lint that enforces it

Three tiers. Primitives are raw scales that no component may name. Semantic roles are the only tier a component reads, and the only tier that differs between light and dark. Component tokens alias semantics and never reach past them.

The rule is worth nothing unless something checks it. The build exits non-zero on a tier violation, on a role defined in one theme and missing from the other, and on any contrast pair below its gate. A custom lint rule bans raw hex outside the token source.

The gate caught eleven failures on its first run. One of them was not a bad colour value but a wrong idea: a sequential ramp can never carry 3:1 between adjacent steps, because that is what makes it sequential. The band boundaries moved onto tick marks and printed ranges, which is where the contrast requirement actually belonged.

Every gated pair, measured · dark theme

66 gated · 2 advisoryfloor 3.15:1generated by the token build
  • Disabled control label (WCAG exempts inactive components; measured anyway)text-disabled on surface-raised2.2n/a
  • Escalate button boundarydecision-escalate-border on surface-raised3.13
  • Band boundary ticksinstrument-tick on instrument-well-bg3.73
  • Confidence span against the wellinstrument-confidence on instrument-well-bg3.73
  • Decline-weighted mark in a chartdecision-decline-vivid on surface-raised4.33
  • Label on the primary buttontext-on-accent on surface-accent4.84.5
  • Ramp range, lowest band against highestinstrument-band-1 on instrument-band-44.93
  • Decline button labeltext-on-accent on decision-decline-solid5.14.5
  • Risk band R4 labelrisk-4-fg on risk-4-bg5.34.5
  • Metadata and column headerstext-tertiary on surface-raised5.34.5
  • Input and outline-button boundary in a panelborder-control on surface-raised5.33
  • Metadata on the app backgroundtext-tertiary on surface-base5.94.5
  • Input boundary on the app backgroundborder-control on surface-base5.93
  • Approve button labeltext-on-accent on decision-approve-solid6.24.5
  • Links and the threshold readouttext-accent on surface-raised6.94.5
  • Focus ring inside a panelfocus-ring on surface-raised6.93
  • Approve-weighted mark in a chartdecision-approve-vivid on surface-raised7.63
  • Focus ring against the shellfocus-ring on surface-base7.73
  • Row metadata under the cursortext-secondary on surface-hover7.74.5
  • Risk band R1 labelrisk-1-fg on risk-1-bg7.74.5
  • Threshold line against the wellinstrument-threshold on instrument-well-bg8.03
  • Supporting text in a paneltext-secondary on surface-raised8.54.5
  • Informational toaststatus-info-fg on status-info-bg9.24.5
  • Declined chipdecision-decline-fg on decision-decline-bg9.64.5
  • Risk band R3 labelrisk-3-fg on risk-3-bg9.94.5
  • Cautionary toaststatus-warn-fg on status-warn-bg9.94.5
  • Risk band R2 labelrisk-2-fg on risk-2-bg10.44.5
  • Escalated chipdecision-escalate-fg on decision-escalate-bg10.74.5
  • Approved chipdecision-approve-fg on decision-approve-bg10.84.5
  • Selected row texttext-primary on surface-selected14.54.5
  • Tooltip texttext-inverse on surface-inverse14.84.5
  • Body text in a paneltext-primary on surface-raised15.14.5
  • Body text on the app backgroundtext-primary on surface-base16.94.5
  • Score marker against the wellinstrument-marker on instrument-well-bg17.73
Not an illustration. This table is drawn from the JSON the token build emits, so it cannot drift from the interface or overstate the result. If a token moves, this figure moves with it.

123

primitives

73

semantic roles, × 2 themes

91

component tokens

0

tier violations

rev 06build

The category decides the API

Sixteen components in five categories, and the category is not a filing convenience. It determines what a component is allowed to know.

A primitive takes visual props and nothing else. The moment a Button understands what a decline is, every future consumer inherits fraud vocabulary it did not ask for. Domain components are allowed to be un-reusable, and saying so out loud prevents the usual failure, where everything is flattened into a generic library that fits nothing.

The base is shadcn on Radix rather than a hand-rolled headless layer. Radix supplies the parts that are cheap to get subtly wrong and expensive to find in an audit: indeterminate checkboxes, roving focus, dismissal on Escape.

The category decides the API

Primitives

7 components

Visual props only. Knows nothing about data or domain.

ButtonInputCheckboxBadgeTooltipKbdSkeleton

Compounds

5 components

Accepts data shapes, carries no business meaning.

DataTableFilterPillSegmentedSortHeaderPanel

Patterns

5 components

Encodes a decision. Reusable across domains.

ScoreInstrumentSignalListDecisionBarUndoToastEmptyState

Domain

5 components

Speaks the vocabulary of risk. Deliberately not reusable.

RiskBadgeVerdictStampEntityChipEventTimelineCaseFacts

Layout

3 components

Owns page structure and focus regions.

AppShellQueueRailSplitReview
The rule for each category, and what currently lives in it.

rev 07build

Every state has a story, including the ugly ones

A component that only exists in its default state is a mockup. The states that break a handoff are the ones nobody drew: empty, loading, failed, one item, four hundred items, a customer name too long for the column.

So every component carries a story per applicable state, and the matrix below is the coverage, not an aspiration. Grey cells are combinations that genuinely do not apply, marked rather than quietly skipped, because an unmarked gap and a deliberate exclusion look identical in a screenshot.

Every state has a story

Component by state coverage. 58 stories across 58 applicable combinations.
defaulthoverfocusactivedisabledloadingerroremptylongdark
Buttonstory existsstory existsstory existsstory existsstory existsstory existsnot applicablenot applicablestory existsstory exists
Inputstory existsstory existsstory existsnot applicablestory existsnot applicablestory existsnot applicablestory existsstory exists
Checkboxstory existsstory existsstory existsstory existsstory existsnot applicablenot applicablenot applicablenot applicablestory exists
Panelstory existsnot applicablenot applicablenot applicablenot applicablestory existsstory existsstory existsstory existsstory exists
DataTablestory existsstory existsstory existsnot applicablenot applicablestory existsstory existsstory existsstory existsstory exists
RiskBadgestory existsnot applicablenot applicablenot applicablenot applicablenot applicablenot applicablenot applicablestory existsstory exists
ScoreInstrumentstory existsnot applicablestory existsnot applicablenot applicablestory existsnot applicablenot applicablestory existsstory exists
SignalListstory existsnot applicablenot applicablenot applicablenot applicablestory existsnot applicablestory existsstory existsstory exists
DecisionBarstory existsstory existsstory existsstory existsstory existsnot applicablenot applicablenot applicablenot applicablestory exists
UndoToaststory existsnot applicablestory existsnot applicablenot applicablenot applicablenot applicablenot applicablestory existsstory exists
story existsnot applicable58 of 58 applicable combinations
Ten states across ten components. The interesting column is not default, it is long-content.

rev 08build

Two flows, two opposite philosophies

Working a case is optimised for speed. Moving the threshold is optimised for consequence. They sit in one system and behave nothing alike, and that is the point.

Consistency in a design system means the same components and the same vocabulary. It does not mean the same ceremony regardless of what is at stake. A tool that treated these two moments identically would be wrong twice.

Working a caseMoving the threshold
Commits immediatelyStaged, then confirmed
Six-second undoNo undo, so it asks first
Single-key shortcuts, A, D, ENo shortcuts at all
No reason requiredA written reason, or it will not apply
Affects one transactionAffects every transaction next week
ReversibleThe customers you turn away do not come back

The threshold screen shows the full distribution of a week of scored transactions, with the overlap visible. There is no setting that catches all the fraud and troubles none of the customers. Making the manager look at that trade, rather than letting them type a number, is the whole design.

rev 09decision

What I hand over, and what I do not

The riskiest claim a designer can make is that they ship production code. So this is stated as a scope with an explicit list of things I did not do.

What I hand a team is design-complete, typed, accessible and running, with mock data. What I do not hand them is a data layer, an auth model, error semantics, a performance budget or a test suite. Those are engineering decisions, and a designer who blurs the line there is asking to be trusted on ground they have not earned.

Treat the repository as a spec that happens to compile. If your stack differs, throw the code away and keep the contract: the tokens, the states, and the accessibility behaviour.

My lane, the shared lane, your lane

I ship this

in the repo, running, testable

  • Typed components on Radix, variants via CVA
  • Tokens as the only source of colour, enforced by lint
  • Keyboard paths, focus states, roles and labels
  • Every state as a story, including the ugly ones
  • Motion tokens, reduced-motion honoured
  • Mock data covering empty, single, excessive, failed

We settle this together

a conversation, not a handoff

  • Which states the real API can actually produce
  • What an error means to a user, versus what it means to a server
  • Where my component boundaries meet your data boundaries
  • Naming, once your domain vocabulary meets mine

Your engineers own this

I do not touch it, and I say so

  • Data fetching, caching, state architecture
  • Auth, permission enforcement, sessions
  • API contracts, error taxonomy, retry and backoff
  • Rendering strategy, performance budgets, bundle splitting
  • Security, sanitisation, rate limiting
  • Test coverage beyond interaction stories
  • i18n extraction and full RTL
  • Analytics, consent, the release pipeline
The middle column is the one that matters in practice. Most handoff failures live there, not at either edge.

rev 10reflection

What building alone changed

For most of my career a Figma file was the deliverable. Someone else turned it into software, and the gap between what I drew and what shipped was somebody else's problem to explain.

Working without an engineering team on the other side of a handoff removed that comfort. A file stopped being a deliverable and became an argument I still had to prove. Verdict is what that looks like when nobody is going to catch the parts I got wrong: a contrast gate because I will not remember to check, a lint rule because I will reach for a raw hex at midnight, a matrix of states because I know which ones I skip.

The tooling changed too. Generating six screens in an afternoon is not the interesting part. The interesting part is that when artifacts become nearly free, the only thing left that is scarce is knowing which one to keep.

02. A risk review console built end to end, from token architecture to working keyboard flows.

Want this kind of work on your team?

Let's design your hardest surface together.

Next case · 03

RushURL Audit.

Four findings on Sunday, three shipped by Tuesday, on a brand-native keyword page that finally puts the link first.