dApp QA — Pre-Ship Audit
This skill is for review, not building. Give it to a fresh agent after the dApp is built. The reviewer should:
- Read the source code (, , )
- Open the app in a browser and click through every flow
- Check every item below — report PASS/FAIL, don't fix
🚨 Critical: Wallet Flow — Button Not Text
Open the app with NO wallet connected.
- ❌ FAIL: Text saying "Connect your wallet to play" / "Please connect to continue" / any paragraph telling the user to connect
- ✅ PASS: A big, obvious Connect Wallet button is the primary UI element
This is the most common AI agent mistake. Every stock LLM writes a
<p>Please connect your wallet</p>
instead of rendering
<RainbowKitCustomConnectButton />
.
🚨 Critical: Four-State Button Flow
The app must show exactly ONE primary button at a time, progressing through:
1. Not connected → Connect Wallet button
2. Wrong network → Switch to [Chain] button
3. Needs approval → Approve button
4. Ready → Action button (Stake/Deposit/Swap)
Check specifically:
- ❌ FAIL: Approve and Action buttons both visible simultaneously
- ❌ FAIL: No network check — app tries to work on wrong chain and fails silently
- ❌ FAIL: User can click Approve, sign in wallet, come back, and click Approve again while tx is pending
- ✅ PASS: One button at a time. Approve button shows spinner, stays disabled until block confirms onchain. Then switches to the action button.
In the code: the button's
prop must be tied to
from
. Verify it uses
(waits for block confirmation), NOT raw wagmi
(resolves on wallet signature):
grep -rn "useWriteContract" packages/nextjs/
Any match outside scaffold-eth internals → bug.
🚨 Critical: SE2 Branding Removal
AI agents treat the scaffold as sacred and leave all default branding in place.
Important: Contract Address Display
- ❌ FAIL: The deployed contract address appears nowhere on the page
- ✅ PASS: Contract address displayed using component (blockie, ENS, copy, explorer link)
Agents display the connected wallet address but forget to show the contract the user is interacting with.
Important: USD Values
- ❌ FAIL: Token amounts shown as "1,000 TOKEN" or "0.5 ETH" with no dollar value
- ✅ PASS: "0.5 ETH (~$1,250)" with USD conversion
Agents never add USD values unprompted. Check every place a token or ETH amount is displayed, including inputs.
Important: OG Image Must Be Absolute URL
- ❌ FAIL:
images: ["/thumbnail.jpg"]
— relative path, breaks unfurling everywhere
- ✅ PASS:
images: ["https://yourdomain.com/thumbnail.jpg"]
— absolute production URL
Quick check:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsx
Important: RPC & Polling Config
Open
packages/nextjs/scaffold.config.ts
:
- ❌ FAIL: (default — makes the UI feel broken, 30 second update lag)
- ✅ PASS:
- ❌ FAIL: Using default Alchemy API key that ships with SE2
- ✅ PASS: uses
process.env.NEXT_PUBLIC_*
variables
Important: Phantom Wallet in RainbowKit
Phantom is NOT in the SE2 default wallet list. A lot of users have Phantom — if it's missing, they can't connect.
- ❌ FAIL: Phantom wallet not in the RainbowKit wallet list
- ✅ PASS: is in
Important: Mobile Deep Linking
RainbowKit v2 / WalletConnect v2 does NOT auto-deep-link to the wallet app. It relies on push notifications instead, which are slow and unreliable. You must implement deep linking yourself.
On mobile, when a user taps a button that needs a signature, it must open their wallet app. Test this: open the app on a phone, connect a wallet via WalletConnect, tap an action button — does the wallet app open with the transaction ready to sign?
- ❌ FAIL: Nothing happens, user has to manually switch to their wallet app
- ❌ FAIL: Deep link fires BEFORE the transaction — user arrives at wallet with nothing to sign
- ❌ FAIL:
window.location.href = "rainbow://"
called before — navigates away and the TX never fires
- ❌ FAIL: It opens the wrong wallet (e.g. opens MetaMask when user connected with Rainbow)
- ❌ FAIL: Deep links inside a wallet's in-app browser (unnecessary — you're already in the wallet)
- ✅ PASS: Every transaction button fires the TX first, then deep links to the correct wallet app after a delay
How to implement it
Pattern: helper. Fire the write call first (sends the TX request over WalletConnect), then deep link after a delay to switch the user to their wallet:
typescript
const writeAndOpen = useCallback(
<T,>(writeFn: () => Promise<T>): Promise<T> => {
const promise = writeFn(); // Fire TX — does gas estimation + WC relay
setTimeout(openWallet, 2000); // Switch to wallet AFTER request is relayed
return promise;
},
[openWallet],
);
// Usage — wraps every write call:
await writeAndOpen(() => gameWrite({ functionName: "click", args: [...] }));
Why 2 seconds? must estimate gas, encode calldata, and relay the signing request through WalletConnect's servers. 300ms is too fast — the wallet won't have received the request yet.
Detecting the wallet: from wagmi says
, NOT
or
. You must check multiple sources:
typescript
const openWallet = useCallback(() => {
if (typeof window === "undefined") return;
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (!isMobile || window.ethereum) return; // Skip if desktop or in-app browser
// Check connector, wagmi storage, AND WalletConnect session data
const allIds = [connector?.id, connector?.name,
localStorage.getItem("wagmi.recentConnectorId")]
.filter(Boolean).join(" ").toLowerCase();
let wcWallet = "";
try {
const wcKey = Object.keys(localStorage).find(k => k.startsWith("wc@2:client"));
if (wcKey) wcWallet = (localStorage.getItem(wcKey) || "").toLowerCase();
} catch {}
const search = `${allIds} ${wcWallet}`;
const schemes: [string[], string][] = [
[["rainbow"], "rainbow://"],
[["metamask"], "metamask://"],
[["coinbase", "cbwallet"], "cbwallet://"],
[["trust"], "trust://"],
[["phantom"], "phantom://"],
];
for (const [keywords, scheme] of schemes) {
if (keywords.some(k => search.includes(k))) {
window.location.href = scheme;
return;
}
}
}, [connector]);
Key rules:
- Fire TX first, deep link second. Never before the write call
- Skip deep link if exists — means you're already in the wallet's in-app browser
- Check WalletConnect session data in localStorage — alone won't tell you which wallet
- Use simple scheme URLs like — not which reloads the page
- Wrap EVERY write call — approve, action, claim, batch — not just the main one
Audit Summary
Report each as PASS or FAIL:
Ship-Blocking
Should Fix