---
name: orchard-planting
description: Plants ETH on Orchard (orchard.gold), the 25-plot round-based game on Robinhood Chain (chainId 4663), and claims its AAPL payouts. Covers round timing, quoting minAaplOut for the in-transaction ETH-to-AAPL swap, the plantMany and claim call sequences, contract addresses and ABIs, payout and EV math, staking, and revert decoding. Use when asked to plant, play, claim, stake, or read game state on Orchard, or when the request mentions AAPL Orchard, $SEED, plots, the Golden Apple jackpot, or Robinhood Chain 4663.
---

# Orchard — plant ETH, harvest AAPL

Orchard is a 75-second game on **Robinhood Chain (chainId 4663)**. You plant ETH on one of
25 plots; the entry swaps to **AAPL** (Robinhood's tokenized Apple stock) inside your own
transaction and that AAPL is your stake. One plot blooms per round and its planters split the
other 24 plots' stake, minus a 10% rake. Claiming pays 10% juice to players who haven't
claimed yet. A 1-in-1000 roll drops the Golden Apple jackpot into the winning plot.

**This spends real money and most rounds lose.** The chain is the source of truth; every
number below is a `view` you should re-read rather than trust.

Terminology follows the contract — **round**, **plot**, **stake**. The website calls a round
a *harvest* and the round's pot a *crop*.

## Contents

- [Quickstart](#quickstart) — plant in one screen
- [Addresses](#addresses) and [parameters](#parameters)
- [Round clock](#round-clock) · [Planting](#planting) · [Claiming](#claiming)
- [Payout math](#payout-math) · [Strategy and EV](#strategy-and-ev)
- [Reading state](#reading-state) · [Swapper and staking](#swapper-and-staking)
- [Rules](#rules) · [Reverts](#reverts)

---

## Quickstart

Prerequisites: a funded key on chain 4663 (ETH is the native gas token, base fee ~0.08 gwei),
and `viem`. Nothing needs approving — `plantMany` is payable and takes ETH directly.

```ts
import { createPublicClient, createWalletClient, defineChain, http, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const chain = defineChain({
  id: 4663,
  name: 'Robinhood Chain',
  nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
  rpcUrls: { default: { http: ['https://rpc.mainnet.chain.robinhood.com'] } },
});
const ORCHARD = '0xEbB8b167c0992cFdc497A995a8Cf7167acAA0A1A';
const SWAPPER = '0x2e31ef7136ce275f39EC4751A9A382734eD5EA52';

const orchardAbi = parseAbi([
  'function plantMany(uint32 plotMask, uint16 roundCount, uint256 minAaplOut) payable',
  'function genesis() view returns (uint256)',
  'function minPlantEth() view returns (uint256)',
  'function plotStake(uint256 round, uint8 plot) view returns (uint256)',
]);
const swapperAbi = parseAbi([
  'function swapEthForAapl(uint256 minOut) payable returns (uint256 bought)',
]);

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const pub = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ account, chain, transport: http() });
const read = (functionName: string, args?: unknown[]) =>
  pub.readContract({ address: ORCHARD, abi: orchardAbi, functionName, args } as never);

// 1. Target round, from the chain's clock. Sowing is open for the first 60s of each 75s round;
//    inside the last 15s plantMany targets the next round instead of reverting.
const [genesis, minPlantEth, block] = await Promise.all([
  read('genesis') as Promise<bigint>,
  read('minPlantEth') as Promise<bigint>,
  pub.getBlock(),
]);
const since = block.timestamp - genesis;
const round = since / 75n + (since % 75n >= 60n ? 1n : 0n);

// 2. Pick plots (0-24) and size the entry. One slot = one plot for one round.
const plots = [3, 11, 19];
const mask = plots.reduce((m, p) => m | (1 << p), 0);
const ethIn = minPlantEth * BigInt(plots.length); // must be >= minPlantEth * slots

// 3. Quote the swap this very transaction will run, then set your own slippage bound.
const { result: quote } = await pub.simulateContract({
  address: SWAPPER, abi: swapperAbi, functionName: 'swapEthForAapl',
  args: [0n], value: ethIn, account,
});
const minAaplOut = (quote * 9_700n) / 10_000n; // 300 bps, what the web app uses

// 4. Plant, then confirm from the receipt — never from the send alone.
const hash = await wallet.writeContract({
  address: ORCHARD, abi: orchardAbi, functionName: 'plantMany',
  args: [mask, 1, minAaplOut], value: ethIn,
});
const receipt = await pub.waitForTransactionReceipt({ hash });
console.log(receipt.status, 'planted round', round);
```

Then wait ~75 seconds and [claim](#claiming).

---

## Addresses

| Contract | Address |
|---|---|
| Orchard (the game) | `0xEbB8b167c0992cFdc497A995a8Cf7167acAA0A1A` |
| OrchardLens (read helper) | `0x7b4B7418b1E15AE55c935956408A8c5007F1F48a` |
| SeedSwapper | `0x2e31ef7136ce275f39EC4751A9A382734eD5EA52` |
| SeedStaking | `0xd5d5f5Dff96E53fc6337b4aCf549d61b12882F2b` |
| Randomness (`Arb2935Randomness`) | `0x312A59cA4e000eBE88080Cb34fEF9984462e5BfC` |
| AAPL — the stake and every payout | `0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9` |
| SEED — staking and buyback token | `0x5eED45d9cD4c21280db5b190D9c263f086401b9D` |
| USDG (6 dec, hop of entries above 5 ETH) | `0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168` |
| WETH | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` |
| Uniswap V3 WETH/AAPL 0.05% — where entries buy their AAPL | `0x8bb3514e2204E1cDF3Ac149EFEe7Ff04D91B719f` |
| Uniswap V3 WETH/USDG 0.05% and AAPL/USDG 0.05% — the route above 5 ETH | `0x69BfaF19C9f377BB306a89aEd9F6B07e2c1a8d9a` · `0xAae0d815EE56e4092a5E5C2911E676Fea50B2d6D` |
| SEED/WETH V2 pair | `0x31331be922EA75707b31802b19097332ad9d9026` |
| Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` |
| Orchard deploy block | `16539672` — anchor log scans here |

RPC `https://rpc.mainnet.chain.robinhood.com` (public, rate-limited; bring your own provider
for sustained load). Explorer `https://robinhoodchain.blockscout.com`. Amounts are wei /
18 decimals unless noted. AAPL and ETH are both 18-decimal and easy to confuse: **`msg.value`
is the only ETH figure; stake, crop, jackpot and payouts are all AAPL.**

## Parameters

Constants, safe to hardcode:

| | |
|---|---|
| `PLOTS` | 25 (ids 0–24) |
| `ROUND_SECONDS` / `SOW_SECONDS` | 75 / 60 — the last 15s are processing |
| `MAX_PLANT_SLOTS` | 250 — cap on plots × rounds per `plantMany` |
| `JUICE_BPS` | 1000 — 10% claim fee, redistributed to players |
| `STAKERS_BPS` | 1000 — 10% of the rake to SEED stakers |
| `genesis` | `1784734757` — immutable clock anchor |

Owner-settable — **read these at runtime**, don't hardcode. Each is a `view` on the Orchard;
the values shown are a snapshot, not a guarantee. All are bounded, and the last three are
frozen per round at seal time, so a change never re-prices a round already in flight.

| | Snapshot | Bound |
|---|---|---|
| `minPlantEth` | 0.0005 ETH **per slot** | ≤ 1 ETH |
| `rakeBps` | 1000 (10% of the losing stake) | ≤ 2000 |
| `adminFeeBps` | 100 (1% of AAPL bought) | ≤ 500 |
| `jackpotBps` | 5000 (50% of the rake) | ≤ 9000 |
| `jackpotOdds` | 1000 → 1-in-1000 per winning reveal | ≥ 1 |

## Round clock

Round ids come from the chain clock, not a counter. Use the node's timestamp
(`eth_getBlockByNumber("latest")`); local time drifts into reverts.

```
round  = (block.timestamp - genesis) / 75
offset = (block.timestamp - genesis) % 75
offset  0..59  sowing open — stakes land on `round`
offset 60..74  processing — stakes frozen; `plantMany` targets `round + 1`
```

During processing the round **seals** (commits to an L2 block 30 blocks out, ~3s) and then
**reveals**: `winningPlot = keccak256(blockhash(sealBlock), roundId, randomnessModule) % 25`.
The committed block does not exist when the commitment is made, so no player can bias the
draw. Whoever calls `crank()` does this work — a keeper runs it continuously and every
`plant` cranks 2 pending rounds, so you never have to.

---

## Planting

Follow this sequence exactly; steps 3 and 4 are where agents lose money.

```
- [ ] 1. Read genesis + minPlantEth; compute the target round from the node's timestamp
- [ ] 2. Read plotStake(round, 0..24) to see the board; pick plots and size ethIn
- [ ] 3. Quote: simulate swapEthForAapl(0) with value = ethIn
- [ ] 4. Send plantMany(mask, rounds, minAaplOut) with value = ethIn
- [ ] 5. Wait for the receipt and parse PlantedMany for startRound and totalStake
```

**Use `plantMany` as the default entry**, even for a single plot
(`plantMany(1 << plot, 1, minOut)`). It behaves identically but rolls into the next round
instead of reverting when the sowing window has closed.

```solidity
// slots = popcount(plotMask) × roundCount, capped at 250; msg.value >= minPlantEth × slots.
// The purchase is split equally across slots (dust to the first).
function plantMany(uint32 plotMask, uint16 roundCount, uint256 minAaplOut) payable

// Single plot, current round only. Reverts SowingClosed in the last 15 seconds.
function plant(uint8 plot, uint256 minAaplOut) payable
```

Both wrap the ETH and buy AAPL straight from the Uniswap V3 WETH/AAPL 0.05% pool (entries above
5 ETH go WETH → USDG → AAPL through the two 0.05% pools instead), measure the AAPL that actually
arrived, re-check it against your `minAaplOut`, carve the 1% admin fee, and stake the rest.
Planting repeatedly in the same round is allowed and simply adds stake.

### Quoting `minAaplOut`

There is no oracle: `minAaplOut` is **your** bound on **your** swap. Passing `0` accepts any
price; passing a stale number reverts. Quote by simulating the exact call the plant will make
— the simulation runs the real pool swap, so the figure already carries the pool fee and
price impact:

```ts
const { result: quote } = await pub.simulateContract({
  address: SWAPPER, abi: swapperAbi, functionName: 'swapEthForAapl',
  args: [0n], value: ethIn, account,   // account must hold ethIn, or override its balance
});
const minAaplOut = (quote * (10_000n - slippageBps)) / 10_000n;
```

The web app uses 300 bps; the house bot uses 100. **Re-quote for every attempt** — a quote is
stale within a block. On `InsufficientOutput` (`0xbb2875c3`), re-quote and retry; do not
widen slippage blindly.

## Claiming

`claim` is all-or-nothing: one unresolved or already-claimed id reverts the whole batch. Build
the list from the lens rather than guessing, and batch — don't pay the fee on dust every round.

```ts
const LENS = '0x7b4B7418b1E15AE55c935956408A8c5007F1F48a';
const lensAbi = parseAbi([
  'function claimable(address orchard, address player, uint256 fromRound, uint256 toRound) view returns (uint256 total, uint256[] roundIds)',
]);
const [total, ids] = await pub.readContract({
  address: LENS, abi: lensAbi, functionName: 'claimable',
  args: [ORCHARD, account.address, fromRound, toRound],
});
if (ids.length) await wallet.writeContract({
  address: ORCHARD, abi: parseAbi(['function claim(uint256[] roundIds)']),
  functionName: 'claim', args: [ids],
});
```

`claimable` loops on-chain: keep each call to **≤ ~2000 rounds** (about 41 hours of play);
wider ranges exhaust `eth_call` gas and revert with empty data. `GET
https://orchard.gold/api/rounds?player=0x…` returns `{ ids: [...] }` scanned from launch if you
want the whole history in one request.

Claiming charges **10% juice** on the gross, paid to players who haven't claimed yet, pro-rata
to their unclaimed winnings — never to the house. Waiting therefore accrues yield; the last
claimer's fee replants the jackpot. Void refunds are untaxed.

---

## Payout math

With `T` = the round's total stake, `P` = stake on the blooming plot, `s` = your stake on it:

```
yourGross = s + (T - P) × (1 - rakeBps/10000) × (s / P)
yourNet   = (yourGross + juiceAccruedSinceReveal) × 90%
```

`pendingClaim(player, roundIds)` returns `yourNet` directly. Three outcomes at reveal:

- **Blooming plot has planters** → the split above. The rake goes 10% to SEED stakers, 50% to
  the Golden Apple, 40% to a buyback that burns SEED.
- **Blooming plot is empty** → no rake, no winners: the **entire round's stake goes to the
  Golden Apple**. This is the cost of covering few plots.
- **Nobody planted**, or the reveal expired (100k blocks without a usable block hash) → the
  round is **voided**; every planter claims their AAPL back in full.

The Golden Apple is an uncapped AAPL pot fed by half of every rake, every empty-plot round, and
each window's last claim. Every reveal *with winners* rolls
`keccak256(seed, "JACKPOT") % jackpotOdds == 0`; on a hit the whole pot joins that round's
payout and is split like any other stake. No owner function can reach it.

## Strategy and EV

The draw is uniform: **every plot has the same 1-in-25 chance**, whatever sits on it. Stake
changes your share, never your odds.

```
EV(stake)   = (1/25) × [ s + 0.9 × (T - P) × (s / P) ]
EV(jackpot) = (1/25) × (1/jackpotOdds) × jackpotAccrued × (s / P)
```

Break-even on stake alone needs `(T - P) / P >= 26.7` — **your plot must hold under ~3.6% of
the round's total**. An evenly spread field puts every plot at 4%, returning ~0.90 per unit:
exactly the rake. Edge, when it exists, comes from planting where the field is not, plus the
jackpot term, minus entry costs (1% admin, both DEX legs and price impact in, the same again
if you convert AAPL back to ETH, and the 10% juice at claim).

Judgement calls, in rough order of impact:

- **Read the board late.** Stakes are public on landing and frozen at 60s, so planting at
  ~45–55s prices `P` almost exactly. There is no public mempool, so lateness costs only
  inclusion risk.
- **Concentration vs spread.** One plot: 1-in-25, large. All 25: you always win, mostly your
  own money back minus fees. Spread's real value is that a plot you cover can never bloom
  empty and vault the whole round to the jackpot.
- **`plantMany` across future rounds is safe** — a round's stake freezes before it opens — but
  you are pricing a board you cannot see.
- **Claim late, in batches.** Unclaimed winnings collect juice from everyone who claims after
  you.

---

## Reading state

Plain `view`s on the Orchard; batch through Multicall3.

```solidity
function currentRound() view returns (uint256)
function nextToResolve() view returns (uint256)   // rounds below this are resolved
function plotStake(uint256 round, uint8 plot) view returns (uint256)        // the board
function stakeOf(uint256 round, address player, uint8 plot) view returns (uint256)
function playerTotal(uint256 round, address player) view returns (uint256)
function playerPlots(uint256 round, address player) view returns (uint32)   // bitmask
function miners(uint256 round) view returns (address[])
function roundClaimed(uint256 round, address player) view returns (bool)
function pendingClaim(address player, uint256[] roundIds) view returns (uint256)
function jackpotAccrued() view returns (uint256)
function juiceIndex() view returns (uint256)

function rounds(uint256 round) view returns (
  uint64 sealBlock, bool revealed, bool voided, uint8 winningPlot,
  uint16 rakeBps, uint16 jackpotBps, uint32 jackpotOdds,   // frozen at seal
  uint256 totalStake, uint256 winningStake,                // winningStake 0 + revealed = bloomed empty
  uint256 netLossPool, uint256 entryIndex, uint256 jackpotWon
)
```

The lens also exposes `jackpots(orchard, fromRound, toRound)` returning
`(round, amount, winningPlot, winningStake, winner, winnersCount)` per hit.

Events — the chain produces ~10 blocks/second, so keep `eth_getLogs` ranges tight:

```solidity
event Planted(address indexed player, uint256 indexed round, uint8 plot, uint256 ethIn, uint256 stake);
event PlantedMany(address indexed player, uint256 indexed startRound, uint16 roundCount,
                  uint32 plotMask, uint256 ethIn, uint256 totalStake);
event Sealed(uint256 indexed round, uint64 sealBlock);
event Revealed(uint256 indexed round, uint8 winningPlot, uint256 winningStake, uint256 netLossPool);
event CropVaulted(uint256 indexed round, uint8 winningPlot, uint256 crop);
event RoundVoided(uint256 indexed round);
event JackpotHit(uint256 indexed round, uint256 amount);
event Claimed(address indexed player, uint256 aaplAmount, uint256 juiceFee);
```

Two HTTP conveniences, cached and read-only. The chain remains the source of truth and these
may change without notice:

```
GET https://orchard.gold/api/explore          ~30s snapshot: recent rounds, jackpot hits,
                                              buybacks, staking flushes, top planters and
                                              stakers, SEED supply and burn. Wei as strings.
GET https://orchard.gold/api/rounds?player=0x…  { ids: [...] } — rounds with something claimable
```

## Swapper and staking

```solidity
// SeedSwapper — token-in legs pull with transferFrom, so approve first.
function swapEthForAapl(uint256 minOut) payable returns (uint256 bought)   // also the quoter
function swapAaplForEth(uint256 aaplIn, uint256 minOut) returns (uint256 ethOut)
function swapEthForSeed(uint256 minOut) payable returns (uint256 bought)
function swapSeedForEth(uint256 seedIn, uint256 minOut) returns (uint256 ethOut)
function swapAaplForSeed(uint256 aaplIn, uint256 minOut) returns (uint256 bought)
function swapSeedForAapl(uint256 seedIn, uint256 minOut) returns (uint256 bought)

// SeedStaking — stake SEED, earn the stakers' 10% of the rake, paid in AAPL.
function stake(uint256 amount)      // approve SEED to the staking contract first
function unstake(uint256 amount)    // no lock, no penalty
function claimReward()
function earned(address) view returns (uint256)
function stakedOf(address) view returns (uint256)
function totalStaked() view returns (uint256)
```

Quote every swap the same way you quote a plant. SEED charges 3% on its pair, buy and sell;
simulated quotes already include it. Staking rewards arrive when the keeper flushes, and wait
in a buffer until `totalStaked` reaches `MIN_RELEASE_STAKE` (10 000 SEED).

---

## Rules

1. **The stake is AAPL, not ETH.** Equal ETH plants in different rounds buy different stakes.
2. **Always pass a freshly simulated `minAaplOut`.** Never `0`, never a cached number.
3. **Prefer `plantMany`.** `plant()` reverts in the last 15 seconds of a round.
4. **`msg.value >= minPlantEth × slots`**, `slots = popcount(plotMask) × roundCount <= 250`.
5. **`claim` reverts as a batch.** Build ids from the lens or `/api/rounds`.
6. **An empty blooming plot pays nobody** — the whole round feeds the jackpot. Only voided
   rounds refund.
7. **Confirm from the receipt**, and check `playerTotal(round, you)` before any retry so a
   timed-out send is not planted twice. Re-send on the **same nonce** rather than queueing.
8. **Round timing is `block.timestamp`** anchored at `genesis`. Never local time, never block
   counts.
9. **AAPL is a tokenized security issued by Robinhood.** The issuer can pause transfers, block
   addresses, burn balances from any address, and upgrade the token — including the Orchard's
   own reserve. Documented, unmitigated risk.
10. **Most rounds are losses.** Plant only what the operator can afford to lose, and stop when
    a stated budget is spent.

## Reverts

| Selector | Error | Cause |
|---|---|---|
| `0x2a20b59e` | `BelowMinPlant()` | `msg.value < minPlantEth × slots` |
| `0x6ec7afbd` | `SowingClosed()` | `plant()` inside the 15s processing window |
| `0xbb2875c3` | `InsufficientOutput()` | swap returned less AAPL than `minAaplOut` — re-quote |
| `0xc0c9cf4e` | `InvalidPlot()` | plot ≥ 25 |
| `0xea35ecb2` | `InvalidPlotMask()` | mask is 0 or sets bits above plot 24 |
| `0xde50091f` | `InvalidRoundCount()` | `roundCount == 0` |
| `0xc74bf510` | `TooManySlots()` | plots × rounds > 250 |
| `0x2733e1c8` | `RoundNotResolved(uint256)` | that round has not revealed or voided yet |
| `0xb3167bfa` | `AlreadyClaimed(uint256)` | you already claimed that round |
| `0x969bf728` | `NothingToClaim()` | the batch pays out zero |

---

Contracts are verified on Blockscout; the rules above are the contract's behaviour, not a
statement of intent. support@orchard.gold · [@orcharddotgold](https://x.com/orcharddotgold)
