// the problem
Onboarding is where Robinhood Chain dApps lose people
A visitor arrives with a wallet, without your network, and with nothing on it. Three things have to happen before your app can do anything, and each one has its own way of going wrong.
They have a wallet
But maybe several, maybe locked, maybe one that predates EIP-6963 and only sets window.ethereum.
Not your network
The switch fails with 4902 because the wallet has never heard of chain 4663. That is the normal case, not an error.
Zero balance
Connected, correct chain, and still unable to send anything. Bridging is a separate journey off your site.
And it can fail anywhere
4001 rejected, -32002 prompt already open, 4200 unsupported, add refused. Most flows render a blank box.
// the state machine
Every state is designed, and the type says so
OnboardingStatus is an exhaustive union. A switch over it with
no default stops compiling the moment a state goes unhandled, so the states
people actually hit first cannot be the ones that were skipped.
Connect
idlethe only status rendered on a serverdetectinglistening for EIP-6963 announcementsno-walletinstall prompt plus a rescandisconnectedconnect button, or a picker for several walletsconnectinga wallet prompt is openlockedits own state, with its own recovery
Network
wrong-chainnames both chain IDs, offers switch and addadding-chainwallet_addEthereumChainin flightswitching-chainwallet_switchEthereumChainin flight- 4902 triggers add, then retries the switch, automatically
- a
-32603envelope is unwrapped to find the real code
Fund
checking-balancenative and USDG, read togetherunfundedbridge routes, address, QR, auto-refreshreadyconnected, on chain, fundederrorthe failure, its hint, and where to retry- a failed balance read never reports a false zero
// evidence
The parameter object is the whole contract
Adding a network is one RPC call carrying one object. A zero-padded chainId,
a currency symbol outside two to six characters, or a declared chain ID that disagrees
with what the RPC answers, and MetaMask refuses. Silently, in every dApp that copied the
snippet. Both objects ship exactly, and the test suite asserts them literally.
import { hoodMainnet } from 'hood-connect'
hoodMainnet.addChainParameter
// {
// chainId: '0x1237',
// chainName: 'Robinhood Chain',
// nativeCurrency: {
// name: 'Ether', symbol: 'ETH', decimals: 18
// },
// rpcUrls: [
// 'https://rpc.mainnet.chain.robinhood.com'
// ],
// blockExplorerUrls: [
// 'https://robinhoodchain.blockscout.com'
// ],
// }
- ✓
0x1237and0xb626decode to 4663 and 46630, unpadded and lowercase, as wallets require. - ✓ Every field matches viem's official
robinhoodandrobinhoodTestnetdefinitions, the same source wagmi uses. - ✓ Symbols sit inside the two-to-six character window MetaMask enforces; decimals are 18.
- ✓ Every URL is https with no trailing slash.
- ✓ The objects are frozen, so a consumer cannot mutate the copy everyone shares.
- ✓ A change to any field fails
npm testrather than a user's wallet.
// surfaces
Four ways in, one machine underneath
The core is framework-agnostic and depends on nothing but viem's types. React, wagmi, and the custom element are thin layers over the same state machine and the same pure state-to-view projection, so they cannot drift apart.
React hood-connect/react
A drop-in component and headless hooks. SSR-safe: renders idle on the server, hydrates without a warning, and inlines its own styles so there is no unstyled flash.
import { HoodConnect } from 'hood-connect/react'
<HoodConnect
config={{ chain: 'mainnet' }}
theme="auto"
onReady={(s) => console.log(s.address)}
/>
wagmi hood-connect/wagmi
The connector kit wagmi does not ship. Chains, transports, and a switchChain that adds the network before switching, in one import.
import { createHoodConfig }
from 'hood-connect/wagmi'
export const config = createHoodConfig({
networks: ['mainnet'],
ssr: true,
})
Web component hood-connect/element
A real custom element in a shadow root. Attributes in, DOM events out, no framework and no build step.
<hood-connect chain="mainnet"></hood-connect>
el.addEventListener(
'hood-connect:ready',
(e) => console.log(e.detail.address)
)
Core hood-connect
The machine on its own, for any framework or none. Actions resolve with the new state and never reject on a wallet failure, so a click handler cannot leak one.
import { createOnboarding }
from 'hood-connect'
const o = createOnboarding()
o.subscribe(render)
o.start()
// step three
Funding routes, not a bridge contract
Robinhood Chain is an Arbitrum Orbit chain, so its canonical deposit path is the Arbitrum portal, not a contract this package could call. Nothing here moves funds, signs, or holds a key. The mainnet routes are the ones the chain's own bridging docs list.
| Route | Destination | Notes |
|---|---|---|
arbitrum-canonical |
portal.arbitrum.io/bridge |
Trust-minimised, inherits Ethereum security, around 10 minutes |
relay | relay.link/bridge/robinhood | Seconds, for a small fee |
across | across.to/?to=robinhood | Intent-based, most EVM chains |
stargate | stargate.finance | LayerZero, useful for stablecoins |
receive |
The user's own address | Always present, always last. EIP-681 URI pinned to @4663, plus a QR |
The receive route ships on every network and under every configuration,
because it is the only one that cannot break: anyone who can get ETH anywhere can send it
to their own address. Its URI carries the chain ID, which is what keeps a scanned code
off Ethereum mainnet. The testnet has no documented public bridge, so there
receive is the only default and you supply your own routes rather than
finding a broken link.
// failure handling
Wallet error codes, normalised once
Wallets disagree about how to report the same condition. Several wrap the real code in a
generic -32603, and some only say it in prose. Every failure becomes one of
a small closed set, each with a recovery hint written for a user and a
retryable flag so the UI never offers a pointless button.
| Provider | Code | What the user is told |
|---|---|---|
4001 | user-rejected | You dismissed the wallet prompt. Try again when you are ready. |
4902 | chain-not-added | Handled automatically: the network is added, then the switch retried. |
-32002 | request-pending | A wallet prompt is already open. Finish it, then retry. |
4200, -32601 | unsupported-method | This wallet cannot add networks from a website. Switch manually. Not retryable. |
4100 | unauthorized | This site is not authorised. Reconnect from your wallet. |
4900, 4901 | wallet-disconnected | Your wallet lost its connection to the chain. |
| empty accounts | wallet-locked | Its own state, not a generic error: unlock, then retry. |
// quickstart
Three lines to a working flow
$ npm install hood-connect viem
$ npm install qrcode-generator # optional, adds the QR to the funding step
import { HoodConnect } from 'hood-connect/react'
export function Page() {
return <HoodConnect config={{ chain: 'mainnet' }} theme="auto" />
}
Node 20 or newer. viem is a peer dependency; react,
wagmi, @wagmi/core, and qrcode-generator are
optional peers, so the base install stays small. Full wiring for Next.js, wagmi, and a
plain page is in
INTEGRATION.md.