# Interns Desks (Anvil AMM) - Integration Guide

Version: 2026-09-28 (three chains: Robinhood Chain, Ethereum, ApeChain)

Interns desks are user run NFT order books with a price ladder, live on Robinhood Chain
(chain id 4663), Ethereum mainnet (1) and ApeChain (33139). One router contract
(InternExchange) per chain owns a registry of desk clones (InternPool). Anyone can open a
desk for ANY ERC-721 on the chain, quoted in the chain's native coin or one of the allowlisted
ERC-20 quote tokens, and traders buy from the cheapest ask or sell into the highest bid. This
guide is for trading terminals, indexers and NFT teams that want their collection on the desk.

Live surface: https://stonkbrokers.io/marketplace?tab=desks (chain switch on the tab)
Docs: https://stonkbrokers.io/docs#desks

The three deployments run the SAME bytecode and ABI (Sourcify exact match on every chain,
Blockscout verified on Robinhood). Only the quote menu, the fee sinks and the listing floors
differ per chain. ABIs ship next to this guide: InternExchange.abi.json and InternPool.abi.json.

## Contracts (Robinhood Chain 4663)

| Contract | Address |
|---|---|
| InternExchange (factory + registry + router) | 0xDea32D8AEE85B41A0f320Ff823e4625AAB01f518 (deploy block 70008491) |
| InternPool implementation (EIP-1167 clone target, locked) | 0x01CF86731Fbf668bA5A14bD2eF4DfE06fD77E303 |
| StonkBrokers collection | 0x539CdD042c2f3d93EbC5BE7DfFf0c79F3B4fAbF0 |
| Stonk Interns collection | 0xFc4b0c4F464dC3037cF013934648a8A726d565A5 |
| InternTransferValidator (interns operator allowlist) | 0x36cdfAAC12e849bf27dCba6cBdFe35d35fEE9951 |

Quote tokens (address(0) = native ETH): ETH, STONKBROKER 0xe934e36A439C94017B64a3FecE66AF12099aBF50,
USDG 0x5FC5360d0400A0FD4f2AF552Add042D716f1d168 (6 decimals), AAPL, AMZN, NVDA, GOOGL, SPCX, USAR,
GME, TSLA, PLTR, AMD, GLD, cbBTC (8 decimals). Read `allowedQuote(token)` for the live menu.
The 25% intern leg of ETH and STONKBROKER quoted trades goes straight to the intern Clock In
engine; for every other quote it lands on the ops converter wallet and is sold and forwarded.

## Contracts (Ethereum mainnet 1)

| Contract | Address |
|---|---|
| InternExchange (Ethereum) | 0xE272B47AF5d8de38E7D09c048a81480290478193 (deploy block 26036272) |
| InternPool implementation (Ethereum) | 0x0020C1b3a52d136cc0f95Db6De6367Da4Ad7b518 |

Quotes: ETH (address(0)), WBTC 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 (8),
USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7 (6), USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (6),
APE 0x4d224452801ACEd8B2F0aebE155379bb5D594381 (18), XAUt 0x68749665FF8D2d112Fa859AA293F07A622782F38 (6).
No listing floors on Ethereum. The 25% intern leg is paid to the ops converter wallet
0xb668382cF44038a3E8140E789060F6A809787CDa and bridged to the Robinhood Chain intern engine.

## Contracts (ApeChain 33139)

| Contract | Address |
|---|---|
| InternExchange (ApeChain) | 0xE272B47AF5d8de38E7D09c048a81480290478193 (deploy block 50227346; same address as Ethereum because the deployer nonce was 0 on both) |
| InternPool implementation (ApeChain) | 0x0020C1b3a52d136cc0f95Db6De6367Da4Ad7b518 |

Quotes: APE (address(0) = native APE), WAPE 0x48b62137EdfA95a428D35C09E44256a739F6B557 (18),
ApeUSD 0xA2235d059F80e176D931Ef76b6C51953Eb3fBEf4 (18), ApeETH 0xcF800F4948D16F23333508191B1B1591daF70438 (18),
USDC.e 0xF1815bd50389c46847f0Bda824eC8da914045D14 (6). No listing floors. The 25% intern leg
lands on the ops converter wallet and is bridged to the Robinhood Chain intern engine. USD marks
in the stonkbrokers.io UI come from the Ethereum Chainlink APE/USD and ETH/USD feeds, never
from an ApeChain feed. Explorer: apescan.io (Blockscout mirror apechain.calderaexplorer.xyz).

The HTTP routes below take `&chain=robinhood|ethereum|apechain` (aliases `4663|1|33139`);
the default is Robinhood.

Note on ERC-721C collections on ApeChain and Ethereum: many top collections run a LimitBreak
or OpenSea operator validator. Those collections trade on a desk only after the validator
allowlists the exchange and its clones (see "Open a desk"); the stonkbrokers.io featured picker
lists validator free collections first for that reason.

## Model

- A desk has one owner, one collection, one quote asset, a type (Buy = bids only, Sell = asks
  only, Trade = both, may add an LP spread up to 20%), a curve (Linear or Exponential), a spot
  and a delta.
- Linear: buying the first NFT costs `spot + delta`, the n-th costs `spot + n * delta`; selling the
  first pays `spot`, the n-th pays `spot - (n - 1) * delta`. Exponential: the same with a 1e18
  multiplier (`delta >= 1e18`): asks multiply, bids divide.
- Buying n at once costs exactly the sum of the n single rungs; the spot moves once per NFT.
- The ladder is per DESK, not per token id: `quoteBuy(pool, n)` prices rungs 1..n from the
  current spot no matter WHICH ids the buyer picks, so the n-th id in a batch always costs the
  n-th rung. A cart that pulls several ids from one desk must price row k at rung k (the
  stonkbrokers.io floor projects rungs over held ids in ascending id order and labels them
  `rung N`); showing every id at the first rung under quotes the batch.
- A Trade (two way) desk that fills a bid re lists the bought NFT immediately: its ask side now
  holds the id and the next ask sits one delta above the new spot (floored collections stay at
  the floor). A Buy desk only holds what it bought until the owner withdraws it; nothing on a
  Buy desk is for sale, so do not render its inventory as listings.
- Fee: `protocolFeeBps` (999 = 9.99%) of the curve value, taken in the quote asset on every trade,
  split `deskShareBps` (6600) back into the desk that filled it, `engineShareBps` (2500) to the
  intern Clock In engine (or a converter for stock / USDG quotes) and the remainder (900) to the
  treasury. Buyers pay `input + lpFee + protocolFee`; sellers receive `output - lpFee - protocolFee`.
- Listing floors: `listingFloor(collection, quote)` is the minimum spot of a Sell / Trade desk.
  StonkBrokers is pinned at 733,332 STONKBROKER (666,666 + 66,666) and `floorGuarded`, so broker
  asks are STONKBROKER only until another quote gets a floor. Bids are never floor gated. A Trade
  desk that buys a floored NFT keeps its next ask at the floor.
- Open collections: `collectionOpen(collection)` is true for any ERC-165 ERC-721 unless the owner
  blocked it. The exchange owner can lower the fee, steer the split (interns keep at least 25%),
  repoint sinks after a 24h delay, pause trading (desk withdrawals never pause) and block a
  collection. No owner path reaches desk inventory or trader funds.

## Reads

```solidity
uint256 n = exchange.poolCount();
address[] memory page = exchange.pools(start, count);        // paged registry
address[] memory mine = exchange.poolsOfOwner(owner);
address[] memory forC = exchange.poolsOfCollection(collection);

InternPool d = InternPool(pool);
d.owner(); d.collection(); d.quote(); d.poolType(); d.curve(); d.spotPrice(); d.delta(); d.lpFeeBps();
d.heldIds(); d.quoteBalance(); d.tradeCount();

(uint256 total, uint256 protocolFee) = exchange.quoteBuy(pool, count);   // trader pays total
(uint256 net,   uint256 protocolFee2) = exchange.quoteSell(pool, count);  // trader receives net
```

`quoteSell` returns `net = 0` when the ladder cannot pay; a Buy / Trade desk can fill a sale only
while `quoteBalance() >= net + protocolFee`.

## Trades

```solidity
// Buy specific ids off one desk. ETH desk: msg.value = maxTotal (surplus refunded).
// ERC-20 desk: approve(exchange, maxTotal) first, msg.value must be 0.
exchange.buy{value: maxTotal}(pool, ids, maxTotal, recipient);

// Several desks in one tx, all sharing the same quote asset.
InternExchange.Swap[] memory swaps = ...; // { pool, ids }
exchange.buyMany{value: maxTotal}(swaps, maxTotal, recipient);

// Sell into a bid: the trader must own the ids and have setApprovalForAll(exchange, true).
exchange.sell(pool, ids, minNet, recipient);
```

Limits: `MAX_BATCH = 20` ids per desk per call. Slippage guards are mandatory (`maxTotal`,
`minNet`); a desk repriced between quote and inclusion reverts `SlippageExceeded` and nothing
moves. Buying a token the desk no longer holds reverts `NotHeld(id)`.

## Open a desk for your collection

```solidity
address desk = exchange.createPool(
  collection,                  // any ERC-721 on the chain (ERC-165 checked)
  quote,                       // address(0) for ETH or an allowlisted ERC-20
  InternPool.PoolType.Trade,   // Buy | Sell | Trade
  InternCurve.Kind.Linear,     // Linear | Exponential
  spot, delta, lpFeeBps,       // ladder; lpFeeBps only on Trade desks (<= 2000)
  initialQuote                 // optional bid funding; ETH via msg.value, ERC-20 pulled (approve first)
);
IERC721(collection).setApprovalForAll(desk, true);
InternPool(desk).depositNFTs(ids);            // asks go live
InternPool(desk).depositQuote(amount);         // fund bids (payable on ETH desks)
InternPool(desk).setParams(spot, delta, lpFeeBps);
InternPool(desk).withdrawNFTs(ids); InternPool(desk).withdrawQuote(amount);
InternPool(desk).transferOwnership(to);        // only with zero NFTs on the desk; then exchange.reindexOwner(desk, oldOwner)
```

Collections with an operator allowlist (ERC-721C style transfer validators) must allow the
exchange AND its desk clones as operators. A registry style validator can consult
`InternExchange.isPool(address)`, which returns true for the exchange and every clone, so no
per desk allowlisting is needed. Plain ERC-721 collections need nothing.

Collection logo: the collection's `owner()` binds a logo by signing the message returned by
`GET /api/desks/collection?address=<c>&message=1&logo=<https url>&nonce=<ms>` and posting
`{ collection, logo, nonce, signature, signer }` to `POST /api/desks/collection`. Without a bind
the desk shows the Blockscout token icon or the first listed NFT's image.

## Events

InternExchange:
- `PoolCreated(pool, owner, collection, quote, poolType, curve, spot, delta, lpFeeBps)`
- `Buy(trader, pool, count, paid, protocolFee)` / `Sell(trader, pool, count, received, protocolFee)`
- `FeeRouted(quote, pool, toDesk, toEngine, toTreasury)`, `FeeParked`, `FeeFlushed`
- `ListingFloor(collection, quote, floor, guarded)`, `CollectionBlocked`, `FeeConfig`, `Wiring`

InternPool (per desk):
- `Bought(recipient, ids, input, protocolFee, lpFee, newSpot)` / `Sold(recipient, ids, output, protocolFee, lpFee, newSpot)`
- `NftsDeposited(ids)`, `NftsWithdrawn(ids)`, `QuoteDeposited(amount)`, `QuoteWithdrawn(amount)`
- `ParamsUpdated(spot, delta, lpFeeBps)`, `OwnershipTransferred(from, to)`

Indexer recipe: scan `PoolCreated` on the exchange from the deploy block in the contracts table
(70008491 on Robinhood, 26036272 on Ethereum, 50227346 on ApeChain) to learn every desk, then
`Buy` / `Sell` on the exchange for volume and fees (`paid` and `received` are in the desk's quote
asset, `protocolFee` is the 9.99% leg; the LP spread stays inside the desk and shows up only in
the per desk `Bought` / `Sold` events). Discovery needs no logs at all: `poolCount()` +
`pools(start, count)` enumerates the registry. Total value locked of a desk = `quoteBalance()`
plus the held NFTs marked at the desk's own next ask. DefiLlama lists the venue as the
`stonkbrokers-desks` child of StonkBrokers (TVL + fees/volume adapters) on Robinhood and Ethereum.

## Revert selectors (plain copy in the desk UI)

CollectionNotAllowed, NotErc721, QuoteNotAllowed, BelowFloor, FloorNotSet, NotAPool,
BatchTooLarge, EmptyBatch, SlippageExceeded, EthMismatch, EthNotAccepted, TransferFailed,
EnforcedPause (exchange); NotOwner, NotExchange, WrongType, NotHeld(id), NotReceived(id),
InsufficientQuote, InventoryHeld, BelowFloor, InvalidDelta, CurveExhausted (desk).

## HTTP read routes (stonkbrokers.io, server aggregated, edge cached)

- `GET /api/desks/floor?chain=robinhood|ethereum|apechain` -> every desk, every listing (per NFT
  ask with rung + USD), every bid (net for the next NFT + depth), collection metadata with logos,
  USD marks per quote, and an `images` map of CDN cached NFT art.
- `GET /api/desks/collection?address=0x...&chain=...` -> ERC-721 check, name, symbol, logo, owner, floors.
- `GET /api/desks/holdings?wallet=0x...&collection=0x...&chain=...` -> token ids the wallet holds (ownerOf verified).
- `GET /api/desks/nft-image/<collection>/<id>?chain=...` -> the NFT image, resized to 512px webp and
  cached on a CDN (IPFS hosted collections are fetched server side; on chain data URIs decoded).
- `GET /api/desks/nft-meta/<collection>/<id>?chain=...` -> decoded metadata / traits for the Buy popup.

These are conveniences for UIs; nothing about the protocol depends on them. Prefer the contract
views for anything that signs.

## Notes for StonkBrokers and Stonk Interns

- Moving an activated broker or intern into a desk, or selling it into a bid, clears its activation
  (the buyer re activates). Dormant interns cannot move at all.
- Interns trade through desks once the collection owner registers the exchange on the
  InternTransferValidator (`setPoolRegistry(interns, exchange, true)`).
- Desk fees are the only fee on this venue: no second royalty layer is charged on desk trades.

Contact: simple@clutch.market. Licensed under BUSL 1.1 (see the repository LICENSE).
