Add network. Fund.
Connect.

The wallet and onboarding kit for Robinhood Chain dApps, chain 4663 and 46630. Thirteen states, three steps, one component. EIP-6963 discovery, the exact EIP-3085 parameters, the 4902 add-then-switch fallback, and a funding step that works even when every bridge is down.

$ npm install hood-connect viem Get started See the states

// 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.

01

They have a wallet

But maybe several, maybe locked, maybe one that predates EIP-6963 and only sets window.ethereum.

02

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.

03

Zero balance

Connected, correct chain, and still unable to send anything. Bridging is a separate journey off your site.

04

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.

1

Connect

  • idle the only status rendered on a server
  • detecting listening for EIP-6963 announcements
  • no-wallet install prompt plus a rescan
  • disconnected connect button, or a picker for several wallets
  • connecting a wallet prompt is open
  • locked its own state, with its own recovery
2

Network

  • wrong-chain names both chain IDs, offers switch and add
  • adding-chain wallet_addEthereumChain in flight
  • switching-chain wallet_switchEthereumChain in flight
  • 4902 triggers add, then retries the switch, automatically
  • a -32603 envelope is unwrapped to find the real code
3

Fund

  • checking-balance native and USDG, read together
  • unfunded bridge routes, address, QR, auto-refresh
  • ready connected, on chain, funded
  • error the 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'
//   ],
// }
  • 0x1237 and 0xb626 decode to 4663 and 46630, unpadded and lowercase, as wallets require.
  • Every field matches viem's official robinhood and robinhoodTestnet definitions, 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 test rather 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.

RouteDestinationNotes
arbitrum-canonical portal.arbitrum.io/bridge Trust-minimised, inherits Ethereum security, around 10 minutes
relayrelay.link/bridge/robinhoodSeconds, for a small fee
acrossacross.to/?to=robinhoodIntent-based, most EVM chains
stargatestargate.financeLayerZero, 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.

ProviderCodeWhat the user is told
4001user-rejectedYou dismissed the wallet prompt. Try again when you are ready.
4902chain-not-addedHandled automatically: the network is added, then the switch retried.
-32002request-pendingA wallet prompt is already open. Finish it, then retry.
4200, -32601unsupported-methodThis wallet cannot add networks from a website. Switch manually. Not retryable.
4100unauthorizedThis site is not authorised. Reconnect from your wallet.
4900, 4901wallet-disconnectedYour wallet lost its connection to the chain.
empty accountswallet-lockedIts 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.