Skip to content

feat(web): the hero shows the flow, and the figures stop reading as a toy - #22

Merged
synacktraa merged 7 commits into
devfrom
feat/hero-flow-bigger-run
Sep 9, 2026
Merged

synacktraa merged 7 commits into
devfrom
feat/hero-flow-bigger-run

Conversation

@synacktraa

@synacktraa synacktraa commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

The hero tile read My servers in the payment path / 0, which asks a reader to already hold the claim before the number means anything. It states the flow instead: what the agent holds, what the allowance holds, what reached the seller.

What is here

hero stat tile 3 parties, each cell captioned with what that party did
lib/demo/params.ts deposit 500_000n to 50_000_000n, 0.05 to 5 USDC
lib/demo/live.ts swap 10 to 100 XLM, destMin 1 to 10 USDC
app/run.tsx rows read from the recording rather than a second list of titles
test/run.test.ts the recording's ids and titles pinned to what the generator emits
lib/dashboard/wallet.ts suggestDefaults, the new-allowance form's opening figures
app/globals.css --lavender-tint #e9e5f7 to #ded7f4
app/opengraph-image.tsx the card's hardcoded copy of the same value
app/layout.tsx description no longer repeats the headline

Findings

A cap against a near-empty balance proves nothing. With 0.05 deposited and a 0.025 cap, the screen gives a reader no way to tell a refusal by the rule from the funds running out, and that distinction is the product. The allowance holds 5 USDC now and the cap stops the agent at 0.5% of it, so one explanation remains. The seller's price is unchanged at $0.01, because a one-cent API call is the reason x402 exists.

The step list had 3 copies. app/run.tsx carried its own array of titles for the pending skeleton, and it went stale the moment the generator changed: it rendered Swap 10 XLM for USDC beside an amount from a 100 XLM swap, on the first screen. It reads the recording's rows now. baked.json is the second copy and drifts the same way whenever a step changes without a re-bake, so a test pins its ids and titles to what the generator emits.

The hero illustrates and the run reports. The run happens in a stranger's browser while they wait, so it stays at 4 asks and 66 seconds; a minute of payments is too few to show what a cap is for. The hero states its own figures. That is why the seller's caption no longer says "what actually moved", which claimed a provenance the cell does not have.

String(1e-7) is '1e-7', which the amount parser refuses. A wallet holding a single stroop would have produced a default the form could not parse, so every suggestion is fixed to 7 decimals and the test hands each one to the parser the form will hand it to.

The tint had no lightness behind it. #e9e5f7 measures 1.016 against the page's cream ground, so hue carried it alone and it read as a rendering artifact. The token moved rather than the one cell, so a refused row and an inactive pill deepen with it, and the card's hardcoded copy moved to match.

The create form

The panel opened at a 1 USDC deposit against a 0.25 cap, small enough that the rule reads as a formality. It offers 5 now, with a deposit of 20 covering 4 of those windows.

Both clamp to what the wallet holds, because a cap the balance cannot reach can never bind. A wallet holding no USDC is offered the plain figures rather than zeros: zeros read as a broken form, and the deploy already states in words that there is nothing to deposit.

wallet holds deposit offered cap offered
100 20 5
17.089855 17.089855 5
6 6 5
3 3 3
none 20 5

Verification

36 tests pass, tsc --noEmit clean, 4 of 4 CI jobs green.

A live run pressed in the browser: 8 rows, both refusals in place, 66 seconds locally and 78 on production.

Contrast measured on every surface the token sits behind, against the old value and the new:

surface                         on old  on new
.pill.off label #4b3f8c            7.1     6.3
OG purple box text #3a2f63         9.6     8.6
.cell .c caption (ink-70)          5.9     5.2
.row.refused body ink             15.5    13.8
refused row vs plain row         1.172   1.318

Caption wrapping measured at 375, 768 and 1440 by text height against line height. The earlier check counted client rects, which returns 1 for a block element however many lines it takes and could not see a wrap at all:

vw 1440   cols 253.672px 174.953px
The agent      1 line   slack 214
The allowance  1 line   slack   5
The seller     1 line   slack  34

The 5px behind the allowance caption is constant from 1100px up, where the layout caps, and predates this branch: the caption it replaced is the same length.

Dead code removed: const allowance = …deploy…hash, unused since the tile was rewritten, and .run + .stats, which no page matches.

Scope

Web only. The contract, the SDK and the seller are untouched. Unaudited, and built for testnet.

synacktraa and others added 2 commits September 10, 2026 02:10
The hero tile said "My servers in the payment path / 0", which asks the reader
to already know the claim before the number means anything. It is three parties
now — the agent, the allowance, the seller — with what each one holds, so the
flow reads left to right and the outcome is the caption under the middle one.
The outcome names which rule stopped each payment rather than totalling them,
and a clause with nothing in it is left out.

The run itself was too small to prove its own point. With 0.05 deposited and a
0.025 cap, a reader cannot tell a refusal from the funds running out; the
allowance now holds 5 USDC and the cap stops the agent well short of it. Eight
payments fit inside the window and a ninth does not, so the agent asks ten
times and takes 0.08 — enough of a run that the cap is visibly a rule and not
an accident. That needed a bigger swap, and a floor under it so a thin book
fails on the swap row rather than two steps later on the deploy.

Two duplicates of the step list went with it. `app/run.tsx` carried its own
copy of the titles and still said "Swap 10 XLM" after the run swapped 100; it
reads the recording's rows now. `baked.json` is the other copy, and a test
pins it to what the generator emits, because a stale recording is wrong on the
first screen and silent about it.

The live run takes about two minutes, not about a minute. Said so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The form opened at a 1 USDC deposit and a 0.25 cap, which is small enough that
the rule reads as a formality. It offers 5 now, and 20 to cover four of those
windows.

Both are clamped to what the wallet holds, because a cap the balance cannot
reach is a rule that can never bind. A wallet holding no USDC is offered the
plain numbers rather than zeros: a form full of zeros reads as broken, and the
deploy is what says in words that there is nothing to deposit.

The suggestion is a function on the wallet rather than a literal in the panel,
so it can be tested. It is: String(1e-7) is '1e-7', which the amount parser
refuses, so every suggestion is fixed to seven decimals and the test hands each
one to the parser the form will hand it to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@synacktraa synacktraa changed the title feat(web): show the payment flow in the hero, over a run worth reading feat(web): the hero shows the flow, and the figures stop reading as a toy Sep 9, 2026
synacktraa and others added 5 commits September 10, 2026 02:22
The link preview arrives as a black box and a lavender one, and the page it
opened was black on white with the lavender nowhere in sight. The allowance
cell carries the tint now, so the pairing a reader clicked through survives
the click.

It is the allowance's colour elsewhere too, which is why this one and not the
seller's: a refused row is tinted because the allowance is what refused it, and
an inactive allowance's pill is the same tint. The refused rows keep their
4px rule and their chip, so nothing reads as refused for being lavender.

Two cleanups in the file while I was in it. The comment above `.run + .stats`
belonged to `.cell.seller`, which went away when the seller cell went back to
left-aligned; the rule itself was dead — no `.stats` follows a `.run` on any
page — so both are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight payments meant every visitor who pressed the button settled nine through
the public facilitator and waited two minutes for it. Back to two payments and
a third refused: four asks, 0.02 to the seller, a 0.025 cap, and 66 seconds
measured in the browser.

The 5 USDC deposit stays. It is what makes the refusal legible — against a
0.05 balance a reader cannot tell the rule from the money running out — and it
costs nothing extra to deposit once. The cap now sits at half a percent of the
balance, which is the same point made harder.

Kept from the larger run: the row list the page derives from the recording
rather than repeating, the test that pins the recording to the generator, and
the swap that funds a 5 USDC deposit with a floor under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--lavender-tint was #e9e5f7, which is a hue with no lightness behind it: 1.016
against the page's cream ground, so on the stat cell it registered as a
rendering artifact rather than a colour. #ded7f4 separates from both grounds —
1.318 against the seller's white, 1.142 against the cream — while the 12.5px
caption keeps 5.2 to 1, well clear of the line C's #d2c9ef was flirting with.

The token, so everything that means "the allowance" moves together: the stat
cell, a refused row in the run, and an inactive allowance's pill. A refused row
now separates from a settled one at 1.318 rather than 1.172, which is the beat
the demo exists for. The OG card's hardcoded copy moves with it, or the page
and the link that opened it drift apart again.

Every surface the token sits behind, re-checked: the pill's label 6.3, the OG
box's plum 8.6, ink on a refused row 13.8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hero's figures were read out of the recording, which tied two things that
want different sizes: the run happens in a stranger's browser while they wait,
so it has to be small and fast, and a minute's worth of payments is too few to
show what a cap is for. The hero states its own numbers now — ten asks, eight
through, one off the list, one over the cap — and the run below states the
run's, with a hash on every row.

The seller's caption said "what actually moved", which claimed a provenance the
cell no longer has. It says what the rules allow instead, which is true of an
illustration and of the contract.

The link preview's description was the headline again. The card already renders
that in type nobody can miss, so the one line under it now says what the thing
is: on-chain spending limits for AI agents that pay for API calls.

Two things fixed on the way. `const allowance = …deploy…hash` had been dead
since the tile was rewritten. And the divider moved to 1.7fr: the allowance
caption had five pixels behind it at every desktop width, which is inside the
jitter of a font that has not loaded yet. It has 21 now, the seller 16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"all the rules allow" reads as though the seller is allowed the rules. The
other two cells say what that party did — the agent asked, the allowance paid
and refused — so this one says it was paid, which mirrors "asked 10 times" and
closes the loop between the two numbers: asked ten, paid eight.

The divider goes back to 1.45fr. That ratio was set by eye and I moved it on my
own initiative to buy the allowance caption room; the caption was the thing to
change, not the column. At 1.45fr the seller has 34px behind its line and the
allowance the 5px it has always had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@synacktraa
synacktraa merged commit 178c474 into dev Sep 9, 2026
4 checks passed
@synacktraa
synacktraa deleted the feat/hero-flow-bigger-run branch September 9, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant