Docs

How a handsel works

Handsel puts a real share of a real stock inside a link. It deploys no contract: a gift is one call to Uniswap's Universal Router, and an opening is two ordinary transfers signed by a key that exists only in the link. This page is what it does, exactly, and what it does not do.

The gift, command by command

Your browser mints one private key per gift and writes it to this browser's storage before anything is sent. Then it builds one transaction to the Universal Router, and reads it back before your wallet is asked to sign:

  1. Pay. In ETH: the router wraps what the swap needs and sells it for USDG, the chain's dollar. In USDG: a Permit2 permit lets the router take that exact amount, once — an approval for the exact number, never unlimited.
  2. Buy. Every dollar the router now holds buys the stock through the pool that pays most — v3 or v4, compared at the same block — and the shares stay inside the router.
  3. Seal. The shares are paid out in equal portions at 1e18 precision: the first envelope takes 1/n of the balance, the next 1/(n−1) of what is left, and the last is swept whatever remains, with a floor. Every envelope gets the same price, because there was one buy.
  4. Gas. Each envelope is sent exactly 0.000038 ETH ($0.09) so it can pay for its own opening.

If any step falls short the whole transaction reverts: no envelope is ever half-filled, and no ETH goes to an envelope whose shares did not arrive. Handsel's own reader (js/route.js, assertGift) refuses to hand the wallet anything that is not exactly this shape — only those commands, every payment to an envelope it just made, each envelope paid its shares once and its gas once.

Measured on 2026-09-16: one link costs $0.06 of gas, ten links in one transaction $0.16, and the pool takes 0.28% of a $25 gift for the median stock.

A link looks like this:

https://<site>/open#h1.<32 bytes of key + 3 bytes of check, base64url>[.<note>]
  • The fragment never leaves the browser. Everything after # is not sent in the request and not put in the Referer header. It reaches this site's own script and nothing else. The pages load no analytics and no third-party script; tools/validate.mjs fails the build if one appears, and if any page ever puts a key in a path or a query.
  • The check is three bytes of keccak256("handsel:v1" ‖ key). Chat apps cut long links. Without a check, a truncated link would derive some other address, find it empty, and tell the recipient their gift was gone. With it, the page says damaged and the gift is still where it was.
  • The note is outside the check, deliberately: a note clipped in transit should cost nobody the gift. It is rendered with textContent, never as HTML, and it is not on the chain — it is in the link.

Opening, and who pays

The recipient gives one thing: an address. Then the envelope's own key signs, in their browser:

  1. one ERC-20 transfer per token the envelope holds, to that address;
  2. whatever ETH is left, less that transaction's own fee.

Every transfer is simulated from the envelope first, and nothing is signed unless all of them pass. The recipient needs no ETH, because the envelope has its own. What is left over — the difference between the budget and what the chain actually charged — goes to them too; the dust that cannot be moved is a fraction of a cent.

The budget is (transfer + sweep) × gas price × ×4, sized so an envelope sealed today still opens after gas has risen. On a fork, an envelope sealed at one gas price was opened at three times that price; at some larger multiple it would be short, and then Handsel says so, names the shortfall, and sends nothing rather than failing half way. Anyone can send the envelope a few cents to unstick it, including the giver.

An envelope that has already been opened is recognised by its nonce: empty and having sent transactions means somebody opened it. Empty and never having sent anything means the gift has not arrived yet. These are different sentences on the page, because they are different situations.

Limits, and why

  • $5 minimum per link: below that, the envelope's own gas is more than 1% of the gift.
  • $2,500 maximum per link: a link is a bearer instrument and a screenshot can steal it. This is a judgement, not a measurement.
  • 10 links in one transaction, so a classroom or a family is one signature.
  • No pool above 5% fee is used at all. This chain carries pools charging 50%, 80% and 90% that hold liquidity and quote happily.
  • A stock is listed only if $1,000 of it fills within 3% through its best pool, at the block it was scanned. 4 candidates were dropped for that reason at the last scan, among them CLSK, CRWV, IONQ.

Addresses

Everything Handsel touches was already on this chain. Each address below was read back on chain by tools/verify-addresses.mjs before it was written into js/config.js, the one place any of them appears.

What was checked, and how

Each of these is a tool in the repository; each has a control that must fail, because a check that stopped checking looks exactly like a clean build.

  • 17/17 properties on Robinhood Chain itself (tools/props.mjs, block 64,747,962): the gift the page builds, run through the real router. Four of them are ABI-surface checks with controls — a tampered gift that pays a stranger, one with a command Handsel never sends, one that pays an envelope twice — each of which must be REFUSED by the page's own reader.
  • 36/36 on a fork of this chain (tools/test-tx.mjs): a gift sent, links made, an envelope opened into an empty wallet, a second opening refused, a gift taken back by its giver, a USDG gift through Permit2, a v4-routed gift, and a starved envelope that is refused cleanly — with a control proving the chain refuses the same transfer, and another proving a top-up fixes it.
  • 2/2 fuzz seeds clean, 10/10 deliberate breaks caught (tools/fuzz.mjs): random sequences of operations on a fork, each break named for the invariant that must notice it. The first run of this found a real fault in a claim on this site rather than in the code — see the notes in CLAUDE.md.
  • 12/12 sabotages of the builder caught (tools/sabotage.mjs), each attributed to the property that noticed; survivors are declared with the reason.
  • 22/22 through the real pages in a real browser (tools/e2e.mjs): a gift made by clicking the app, and opened by clicking the recipient's page, against a fork.

The Solidity harness is contracts/HandselProps.sol. It builds no calldata of its own: it is handed the bytes the page would send and asked whether they do what the page says, so a broken builder fails here. It runs inside an eth_call with no to — real creation code on the real evaluator, costing nothing and leaving nothing behind — with a state override that gives the caller ETH to spend. Init code 8,130 bytes, against EIP-3860's 49,152.

Nothing here has been audited. Handsel is a page over somebody else's contracts, and the whole of it is checked in the open.

What is not built

  • No "take it in dollars" button. A recipient who wants cash moves the shares to their wallet and sells them on any Uniswap front end. Selling from inside the envelope would need two more approvals and a swap, and it is not there.
  • No baskets. One stock per gift. An envelope that is topped up with something else is opened correctly — everything it holds comes out — but Handsel will not buy two stocks in one gift.
  • No expiry and no claw-back. Nothing returns a gift automatically. You take one back by opening it yourself, which needs the link.
  • No account, no email, no notification. Nobody is told that a gift arrived or was opened. The app can only show what the chain says.
  • No printed card, no QR code. Just the link, and a text file of every link this browser made.
  • Nothing is stored anywhere but your browser. Clear site data and your copy of the links is gone. The gifts are unaffected — but so is your ability to find them.