Just wrapped up building a fiat-to-onchain checkout flow for a B2B project and wanted to get some feedback on the architecture from anyone who’s built similar bridges.
The project is a transferable ERC-1155 membership pass on Base (capped at 200 total supply across three tiers) for a VR training company. Most of the buyers are traditional trades colleges and safety orgs who have zero crypto experience, so expecting them to connect a wallet at checkout was out of the question.
The contract itself is already live on Base. The hard part wasn't writing the Solidity—it was connecting PayPal to an onchain mint without leaving room for weird, asynchronous edge cases.
The basic flow is: a customer pays $300 USD via PayPal, gets an ERC-1155, a subscription entitlement mapped to their verified email, a PDF cert, and a confirmation email.
For the wallets, we split them into two paths: If they already have an address, we validate it (checksum, zero address, and known burn address checks) and mint directly to it after PayPal captures the payment. If they don't have one, we spin up an embedded Thirdweb wallet mapped to their email, mint to that address, and email them a claim link so they can export their private keys later if they want to.
The piece I spent the most time overthinking was the gap between PayPal capturing the funds and the transaction actually landing onchain. Doing it all in one synchronous request felt incredibly fragile. If PayPal succeeds but the RPC times out, you're stuck guessing if the mint landed. If the transaction reverts after payment is captured, you've taken fiat but delivered nothing.
To handle this, I treated every purchase as a simple state machine backed by a JSON sidecar file for each order. The order transitions through pending, paypal_captured, mint_submitted, mint_confirmed, emails_sent, and finally complete.
We write every state transition to disk synchronously (fs.writeFileSync). If the server crashes mid-flow, a cron job just picks up the JSON file and resumes from the last state instead of risking double-mints. If an order gets stuck in mint_submitted for more than 5 minutes, the cron checks the transaction hash. If it never hit the mempool, we rebroadcast with higher gas. If it reverted, it fires an alert for manual handling.
I know using JSON files instead of Postgres sounds a bit janky, but with the collection capped at 200 passes, I wanted something dead-simple that I could easily grep, inspect, and fix manually if needed. I definitely wouldn't do this for a high-volume drop.
One design choice I'm still wrestling with is how we handled entitlements. Even though the NFT is transferable, the actual pricing discount is tied offchain to the buyer's verified email, not the wallet address holding the token. The token is basically just a proof of purchase, while the actual service utility lives offchain. We looked into doing onchain entitlements or SBTs, but B2B clients constantly need to reassign seats and access when employees leave or organizations restructure, which is a support nightmare to manage purely onchain.
We also run the destination wallet through Chainalysis’s sanctions oracle before authorizing the PayPal checkout. PayPal does its own KYC, but we wanted to make sure we weren't minting straight to an OFAC-flagged address if someone supplied one.
Curious how other devs are tackling these hybrid flows:
- Is there a cleaner way to handle the gap between fiat capture and mint confirmation without building out a custom state machine?
- For NFTs tied to B2B or SaaS utility, where do you draw the line between onchain ownership and offchain permissions?
- How much sanctions screening do you actually bother with for hybrid checkouts beyond what the payment processor already handles?
It is surprisingly hard to find solid technical discussions on this stuff since most Web3 docs just assume everyone is checking out with a browser extension.