Cross-Chain Identity Verification for Wallets and Socials
Carrying World ID proofs to other chains and social accounts
A proof locked inside World Chain
The work started from a mini app running inside the World App ecosystem. The problem states easily. A user has already proven "I am a human" with an orb or a device, and that fact stays locked inside World Chain. There was no way to attach that proof to a wallet on another chain, to an X account, or to an email address.
There were three asks. First, extend World ID verification to external wallets, social accounts, and contact methods, and leave an on-chain trace of it. Second, make that trace readable by a dApp in one line: isHuman(user, "orb"). Third, let the same identity sign in from a browser outside World App. On top of that sat a token drop that bots cannot claim.
This piece covers only the axis that binds wallets and social accounts to a proof of personhood, plus the edge infrastructure holding it up. Generating three front-end clients from a single spec, and the deploy pipeline that encrypts environment variables and commits them to the repo, are set aside here.

Five workers, one spec
Split every service into five Cloudflare Workers, and reach Postgres only through a Hyperdrive binding
Users are spread across 40 locales, so the moment you pin to one region, the other side of the planet pays for it. Workers run at the edge and placement: smart puts them near the database. Workers have no TCP connection pool. Opening a fresh Postgres connection per request means the database dies first when traffic picks up even slightly. So everything connects through env.HYPERDRIVE.connectionString, and app code never thinks about connection lifetime at all.
Mint SBTs gaslessly with a server-side EIP-712 signature plus an Alchemy smart wallet paymaster
Users could not be asked to fund gas on every chain. The contract's mint is callable by anyone and only verifies the serverSigner signature, so it does not matter who sends the transaction. Giving a relayer account on-chain authority would make that key the minting authority. So HBTPaymaster.mint builds a throwaway smart account per call with LocalAccountSigner.generatePrivateKeySigner(), and the real authority lives only in the signing key.
Compute status flags in Postgres generated columns rather than in the app
If isValid, isDone, and totalAmount are updated by the app, three apps update them at three different moments. walletConnections.isValid is a generatedAlwaysAs column that is true only when both the proof and the signature are attached and deletedAt is empty, so the verdict is the same whichever path the row came in through. The trade is that generated columns are awkward to index and to migrate, so every change to the condition meant rebuilding the column.

Phone proves, server signs, database orders
The whole thing is five Workers and one Postgres. hp-miniapp is the React SPA that runs inside World App, hp-web is the browser console, hp-link is a React Router app that renders shared links with SSR, hp-api is a single API written in Hono, and hp-metadata is a small worker that builds the SBT's tokenURI out of the database. All three front ends use nothing but the client generated from hp-api's OpenAPI spec.

Attaching proof of personhood to a wallet that never opened World App
The most common approach is to try connecting the external wallet inside the mini app. But there is no MetaMask in the World App webview. So the next move is to take a wallet signature on the web, send it to the server, and store it as "this user's wallet." That signature only proves wallet ownership. It proves neither that a human holds that wallet nor that the human owns this account. Even if you fetch a World ID proof separately, there is no way to tell that the proof was about this particular connection.
I bound the two pieces of evidence into a single row. In the browser, wagmi takes an OwnershipProof EIP-712 signature and calls POST /wallet-connections. The server checks that the signature's timestamp is within three minutes, recovers the address with recoverTypedDataAddress, then creates a wallet_connections row with a three-minute expiry and one notification in the same transaction. The notification's href is /verify-wallet-connection/{connectionId}. When the user opens the mini app inbox on their phone and taps that card, the mini app calls MiniKit.commandsAsync.verify({ action: "verify-ownership", signal: connectionId }). The server rejects immediately if signal !== connectionId, and only after the proof passes the World verification API with a signal_hash built from hashToField(connectionId) does it store the proof, issue an hbt_ids number, and mint the SBT. Meanwhile the browser polls the same connection every two seconds and moves the screen forward when it sees status === "COMPLETED".
The connection id is inside the World ID proof's signal, so that proof is valid for this one connection only. It cannot be copied onto another connection, and it cannot be attached twice to the same one (the proofConnectedAt check). On top of that, isValid is a generated column that turns true only when proofId and signId are both present and deletedAt is empty, so a row holding only one of the two pieces of evidence never shows up in any query. From the user's side it is a signature in the browser and one tap on the phone, with no QR code and no deep link.
Making sure a first-come drop never pays the same person twice
Since it is first come first served, putting a counter in the contract and letting the first transaction to arrive win looks like the natural move.
I split it: ordering is decided in one place, the database, and replay is prevented in one place, the chain.
Row locking means ranks are assigned one at a time even when people tap simultaneously. Whoever loses never builds a transaction at all and just receives OUT_OF_RANK along with their own rank, so there is no gas and no failure screen.
What a user can do
The unstored nonce and leftover code
Starting with what I have to admit. Mini app sign-in does not store the nonce and verifies it only with computeHMAC(nonce, JWT_SECRET). That suits Workers, since the server holds no state, but the HMAC carries neither an expiry nor single use, so the same nonce and hmac pair can be replayed. The SIWE message itself has an expiry so the real risk was small, but if I built it again I would put the issue time inside the nonce, sign them together, and guarantee single use with KV.
The second one has no excuse. The pay-link code from an early experiment left the funding account's private key and an Alchemy key sitting in source (packages/012_api/src/services/pay-link.ts, endpoints/links/confirm-deposit.ts). It was a path that stopped being used once the link vault contract took over, but it should have been deleted from the router and the keys revoked. A label saying "temporary code" is a reason to delete it, not a reason to leave it in.
Last, there is not a single test. I pulled in forge-std and still never wrote the contract tests, and there are no contract tests on the API either. On a schedule that shipped 5 workers and 4 contracts in three weeks, that was the first thing cut, and in a system where tokens move on a server signature I think that was the wrong call.












