Skip to content

Paywall: four improvements (balance guard, trustline action, wallet coverage, payment receipt) #74

Description

@pedro-pelicioni

Four things in packages/paywall make a payment harder than it needs to be, or hide something the payer should see. I hit all four while building an x402 client against this repo, and I have a fix branch ready for each — happy to open them as PRs, or to drop any you would rather not take.

To check the claims I rendered StellarPaywall.tsx from source against scripted wallet/RPC state, on main and on each branch, and read the resulting DOM rather than eyeballing screenshots. The numbers below come from that.


1. Pay is enabled when the balance is known to be too low

handlePayment guards on an empty string:

if (tokenBalanceFormatted === "") {
  const freshBalance = await refreshBalance();
  if (Number(freshBalance) < amount) { /* reject */ }
}

useStellarBalance fetches in a useEffect as soon as address becomes non-null, so by the time Pay is clickable that string is populated and the block never runs. A wallet holding 0 USDC on a $0.51 route goes straight to signing and fails afterwards at verify/settle, with an error that does not name the cause.

balance 0n, price 5100000 Pay label disabled
main Pay false
with the fix Insufficient USDC true

Separately, Number(formatted) < amount loses precision above 2^53. Both sides are already atomic units of the same asset, so the comparison can be exact.

2. The trustline banner is a dead end

When the account has no trustline the banner links to:

https://lab.stellar.org/account/fund

That is Lab's friendbot account funding page. It has nothing to do with trustlines, and friendbot does not exist on mainnet. The banner's own text says "add the USDC trustline" — the link does not lead anywhere that can do one.

The wallet that could sign a change_trust is already connected to the paywall, so the banner can just offer the action, with /transaction/build as the fallback link.

3. Four of the ecosystem's wallets are missing

useSWKConnection registers FreighterModule, HanaModule, KleverModule, OneKeyModule. SWK ships a zero-config module for xBull, LOBSTR, Albedo and Rabet too. Anyone holding USDC in one of those cannot pay, and the modal gives no hint their wallet was never an option.

Opening the real SWK modal through each version's own useSWKConnection:

wallets offered
main Freighter, Hana, Klever, OneKey — 4
with the fix xBull, Albedo, Freighter, LOBSTR, Rabet, Hana, Klever, OneKey — 8

Ledger, Trezor and WalletConnect stay out — they need per-integrator configuration and cannot be enabled by id alone.

4. The payer never sees the payment they just made

On success the paywall calls onSuccessfulResponse, and entry.tsx does document.documentElement.innerHTML = await response.text(). The document is replaced immediately: no amount, no recipient, no transaction hash — nothing the payer can check on-ledger.

examples/simple-paywall compensates by templating {{TX_LINK}} into its own paid page (txHashInjector.ts), but that only helps hosts serving HTML they control. A host serving JSON or a file gets nothing.

The PAYMENT-RESPONSE header already carries the settled hash, so the paywall can show a receipt before handing off.


Fixes

Branches, each cut from main, independent of the others, with pnpm test, pnpm typecheck and pnpm lint passing on every commit:

# branch PR
1 fix/74-balance-guard #79
2 fix/74-trustline-action #80
3 fix/74-wallet-coverage #81
4 fix/74-payment-receipt #82

21 tests added in total. Two carry a judgement call I would rather you make than me:

  • feat: update client paywall design #3 adds ~99 KB uncompressed for the four wallets. The list is configurable either way, so shipping four by default and letting hosts opt into the rest is a one-line change if you prefer that.
  • feat: add multi-network support to render both Testnet and Mainnet #4 holds the paid content for 3s to show the receipt. That is the only behavioural change across all four. It is configurable, and 0 restores today's behaviour exactly — say the word and I will ship it as 0 by default so the receipt is opt-in.

Happy to split, reshape or drop any of these. cc @marcelosalloum @jeesunikim

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions