You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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:
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:
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.
Four things in
packages/paywallmake 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.tsxfrom source against scripted wallet/RPC state, onmainand 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
handlePaymentguards on an empty string:useStellarBalancefetches in auseEffectas soon asaddressbecomes 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.0n, price5100000disabledmainPayfalseInsufficient USDCtrueSeparately,
Number(formatted) < amountloses 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:
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_trustis already connected to the paywall, so the banner can just offer the action, with/transaction/buildas the fallback link.3. Four of the ecosystem's wallets are missing
useSWKConnectionregistersFreighterModule,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:mainLedger, 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, andentry.tsxdoesdocument.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-paywallcompensates 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-RESPONSEheader 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, withpnpm test,pnpm typecheckandpnpm lintpassing on every commit:fix/74-balance-guardfix/74-trustline-actionfix/74-wallet-coveragefix/74-payment-receipt21 tests added in total. Two carry a judgement call I would rather you make than me:
TestnetandMainnet#4 holds the paid content for 3s to show the receipt. That is the only behavioural change across all four. It is configurable, and0restores today's behaviour exactly — say the word and I will ship it as0by default so the receipt is opt-in.Happy to split, reshape or drop any of these. cc @marcelosalloum @jeesunikim