Pay with crypto on any site. PYUSD → escrow → virtual card. Simple & secure.

Monkey Bridge is a Browser extension that enables users to pay with PYUSD cryptocurrency on websites that only accept traditional payment methods (credit/debit cards). It uses a secure escrow smart contract to hold funds until payment is processed, then generates virtual card details to complete the merchant checkout.
Building Monkey Bridge was honestly a tough challeng, especially as it was an app unlike anyone ive built before; the intersection between browser extensions, smart contracts and wallets, and traditional payments.
I built this as a three-layer system: a Solidity smart contract for the escrow, a Node.js backend that acts as the bridge, and a Chrome extension for the user interface. The extension is built with React 19 and Vite, styled with Tailwind CSS. I'm using Ethers.js v6 for all blockchain interactions. The backend is Express running on Node, handling transaction verification and virtual card generation through Stripe's API. The main partner tchnology is PYUSD (PayPal USD) as the payment token, which was an interesting choice because it is the official paypal stable currency, which makes even more sense as it opens up many other ways of funding. Now a user who has funds on their paypal and want to pay on a site that do not even support paypal can easily fund their wallet from their paypal account and pay with Monkey-brdge extending even more payment options for paypal users. The integration of paypal's stable coins made it an excellent choice for the overall usage of the app.
The Wallet Detection Problem (The Biggest Hack): This was probably the gnarliest problem I faced. Browser extensions run in an isolated context that's completely sandboxed from the actual webpage. This is great for security, but it means the extension literally cannot see window.ethereum - the object that MetaMask and other wallets inject into web pages. I spent hours trying to access wallet providers from the extension context and kept getting undefined. The extension and the webpage are in completely different JavaScript worlds, even though they're running on the same page. After digging through Chrome extension documentation and Stack Overflow with no luck, I found a discussion thread on Google Developer Groups where someone mentioned injecting a script into the "main world" (the actual page context) instead of the "isolated world" (extension context). This was the key. I had to create a separate injected.js script that gets injected directly into the page's main context:
Chrome Extension Architecture Challenges Chrome Manifest V3 is... restrictive. Very restrictive. You can't load external scripts, you can't use eval(), you can't even use inline scripts. This killed my initial plan to dynamically load Stripe.js for payment processing. I tried workarounds like dynamic script injection, but Chrome's Content Security Policy just wouldn't allow it. Eventually, I had to pivot to a different architecture. The extension bundles everything locally using Vite, and I moved all the Stripe logic to the backend. This actually turned out to be better for security anyway - the extension never handles sensitive card data. Another weird thing about Chrome extensions is that you have three completely isolated JavaScript contexts: the content script (runs in the web page), the background service worker, and the popup/side panel. Getting these three to communicate required building a message-passing system that coordinates everything:
The Auto-Fill Problem: I wanted to auto-fill the virtual card details into the merchant's checkout form automatically so users wouldn't have to copy-paste. Most payment processors use iframes. Stripe Elements, for example, renders the card input fields in a cross-origin iframe. Browser security policies make it literally impossible to access content inside a cross-origin iframe. There's no hack, no workaround - it's just blocked at the browser level. So I compromised: I attempt auto-fill for regular HTML forms (works great on simpler checkout pages), but fall back to showing copy buttons for each field when auto-fill fails. Not ideal, but it's the best I can do given browser security constraints. But i ensured that even at that only amount processed on chain is funded into the issued card attached to that users wallet address, and those cards are saved in users records because cards are issued only once to a user and is utilized through the customer life-cycle unless reissued.
What I'd Do Differently: If I were starting over, I'd probably use Solana or Arbitrum instead of Ethereum. Gas fees on mainnet are brutal - sometimes $20-50 per transaction. Sepolia is free for testing, but production would be expensive on mainnet. I'd also look into privacy-preserving alternatives for the virtual card generation. Right now, the backend sees all the card details, which isn't ideal. Maybe something with zero-knowledge proofs or homomorphic encryption could work, though that's pretty advanced territory.
The Hacky Bits: Probably the hackiest thing I did was the React-aware form filling. I'm literally reaching into React's internals to set values and trigger events. It feels wrong, but it works. The injected script solution for wallet detection is also pretty hacky. I'm injecting code into every webpage, using postMessage to communicate across JavaScript contexts, and hoping nothing breaks. But it's the only way to access window.ethereum from a Chrome extension. Another sketchy thing is how I handle multiple wallets. I'm basically guessing which wallet the user wants based on which one I detect first. There's no perfect solution here - I'd need a wallet selector UI, but that's more complexity. The network auto-detection is also a bit magical. I just checked eth_chainId and mapped it to contract addresses in my config. If the user's on an unsupported network, I show an error. Works fine, but assumes my config is always up to date.
Lastly, the mini-dex. I needed a way to ensure users can always access PYUSD even while holding other tokens. I needed to integrate a pool for swapping assets. The main dex I found trading PYUSD actively is on 1-inch and on mainnet only. So I had to create my own Uniswap pools on sepolia and added liquidity for all the tokens I needed. usdc, usdt, eth. So users can swap the assets they have, right in the extension for PYUSD using the Uniswap router. This will be replaced with actual 1-Inch integration in live production.

