myelin
Robinhood Chain
Docs: Build with it

Build with it

Everything Myelin does is a public function or an event. This page shows how to read the fee, call the model, follow every swap, and open a pool of your own with the hook.

The contracts

ContractWhat it isOn Robinhood Chain
HookSets the fee of every swap in its pools, and keeps each pool’s memory. Immutable, no owner.0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4
ModelThe neural network: a pure function from eight integers to one. Free to call.0xF5400cC2836D462605b5d7877b2C26F3f134E87e
TreasuryReceives the fee share and runs the buyback and burn, inside a price limit.0xEE9B3C0F79e9240D966ad211572cd194cdc36770
Price oracleThe hook of the buyback pools: it records their price, so that the Treasury can measure its limit against the average. OpenZeppelin’s oracle hook, unchanged.0x8d64A1297a1877e482d6d52689DA5417bA0F9080
Pool managerUniswap v4’s own, unchanged.0x8366a39CC670B4001A1121B8F6A443A643e40951

Fees everywhere are in hundredths of a basis point, Uniswap v4’s unit: 3000 is 0.30%. Addresses on other networks are on Networks and addresses.

Read the fee

previewReflex(poolId) does what beforeSwap would do now: the eight inputs, the model’s output and the total fee. It is a free call. The liquidity providers’ part is lpFeeOf(fee); the rest goes to the Treasury. stateOf(poolId) returns the pool’s memory itself: the tick and time of the last observation, and the averages.

The eight inputs, the model output and the fee for a swap in the next block
cast call 0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4 "previewReflex(bytes32)(int256[8],int256,uint24)" 0x7ba739294819cb9e217dfee21164dfcae213365b9efafb4680733a408666c846 --rpc-url https://rpc.mainnet.chain.robinhood.com
TypeScript, with viem
import { createPublicClient, http, parseAbi } from 'viem';

const client = createPublicClient({ transport: http('https://rpc.mainnet.chain.robinhood.com') });
const hookAbi = parseAbi([
  'function previewReflex(bytes32 poolId) view returns (int256[8] features, int256 pred, uint24 fee)',
  'function lpFeeOf(uint24 totalFee) view returns (uint24)',
]);

const [features, pred, fee] = await client.readContract({
  address: '0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4',
  abi: hookAbi,
  functionName: 'previewReflex',
  args: ['0x7ba739294819cb9e217dfee21164dfcae213365b9efafb4680733a408666c846'],
});
console.log('fee', Number(fee) / 10_000, '%');

Follow every swap

After every swap the hook emits Reflex: what the pool remembered, what the model said, the total fee, and the new tick. That is all you need to recompute the swap, including whether it was an observation: the next swap’s inputs change only if it was. Its share goes out in ShareTaken, and a failed model call in ModelFailed.

The events
event Reflex(bytes32 indexed poolId, int256[8] features, int256 pred, uint24 fee, int24 tick);
event ShareTaken(bytes32 indexed poolId, address indexed currency, uint256 amount);
event ModelFailed(bytes32 indexed poolId, uint24 fallbackFee);
TypeScript, with viem: the last 5,000 blocks
import { createPublicClient, http, parseAbiItem } from 'viem';

const client = createPublicClient({ transport: http('https://rpc.mainnet.chain.robinhood.com') });
const head = await client.getBlockNumber();

const logs = await client.getLogs({
  address: '0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4',
  event: parseAbiItem('event Reflex(bytes32 indexed poolId, int256[8] features, int256 pred, uint24 fee, int24 tick)'),
  args: { poolId: '0x7ba739294819cb9e217dfee21164dfcae213365b9efafb4680733a408666c846' },
  fromBlock: head - 5_000n,
  toBlock: head,
});
for (const { args } of logs) console.log(args.tick, args.pred, args.fee);

Public RPC limits

The free RPC refuses queries that match more than 10,000 logs, and it is rate limited by cost, so read in windows of a few thousand blocks and retry with a pause on an error. The block timestamp is not in the log: get it from the block.

Call the model

The model is a pure function of eight integers, in this order. Values outside their range are clamped, so a call never reverts. It costs 21,890 gas inside a transaction and nothing as a call.

#InputWhat it measures
0a_last|Δtick| of the last observation, capped at 4096. 0 until there is one.
1ewFastMoving average of |Δtick| over observations, weight 1/4 each. Reacts within a few observations.
2ewMidThe same average with weight 1/16. The rule-based baseline reads only this one.
3ewSlowThe same average with weight 1/64: the long-run level of movement.
4ewSignedMoving average of the signed Δtick (weight 1/4): which way the price is leaning.
5streakConsecutive observations that moved the price the same way, from 0 to 16.
6dtNowBits in the seconds since the last observation, at the current block: 0 for none, 1 for one second, 2 for 2–3 s, up to 12 for 34 minutes or more.
7dtEwmaMoving average of the seconds between observations (weight 1/8).
Output is ln(1 + expected tick movement in the next 60 s) × 256
cast call 0xF5400cC2836D462605b5d7877b2C26F3f134E87e "predict(int256[8])(int256)" "[12,1212,339,86,-324,1,4,3181]" --rpc-url https://rpc.mainnet.chain.robinhood.com

modelHash() returns the keccak256 of the published weights file. The policy turns the output into a fee: fee = clamp(feeMin + ((slope × max(0, pred − threshold)) >> slopeShift), feeMin, feeMax), with the hook’s immutables 516, 4127, 63, 4 and 274 (see How the fee is set).

Open a pool with the hook

Anyone can open a pool with a deployed hook. The hook asks for two things: that the pool’s fee is the dynamic-fee flag (0x800000, which is 8388608), and, while its share is not zero, that the Treasury can spend both currencies of the pool (it has a buyback pool for each, or one of them is the project token). Otherwise initialising reverts with UnsupportedCurrency. Currencies are sorted by address, with the native currency first; the tick spacing is yours. The deployed pool uses 10. The hook starts the pool’s memory at its first price.

Initialise a pool at a price of 1 (sqrtPriceX96 = 2^96); compute your own for your pair
cast send 0x8366a39CC670B4001A1121B8F6A443A643e40951 "initialize((address,address,uint24,int24,address),uint160)" "(<currency0>,<currency1>,8388608,10,0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4)" 79228162514264337593543950336 --rpc-url https://rpc.mainnet.chain.robinhood.com --private-key $PRIVATE_KEY

Before you do

  • The model and the policy are the ones the hook was deployed with. They were tuned on one pool; the backtest shows how little that says about others.
  • The hook takes 10% of every fee for the Treasury, and the Treasury can only spend currencies it has a buyback pool for, so this hook refuses a pool of other tokens. If you want a different share, Treasury or pair, deploy your own hook.

Deploy your own hook

A different model, band, policy or share means a new hook. The constructor takes the pool manager, the model, the Treasury, the policy and the share:

Solidity
constructor(IPoolManager _poolManager, IMyelinModel _model, address _treasury, Policy memory policy, uint24 protocolShare)

struct Policy { uint24 feeMin; uint24 feeMax; uint24 feeFallback; uint8 slopeShift; uint256 slope; int256 threshold; }
  • The hook’s address must carry the permission bits 0x30c4 (before and after initialize, before and after swap, and the swap return delta). Mine a salt for it with Uniswap’s HookMiner against the CREATE2 deployer 0x4e59…956C, as contracts/script/DeployMyelin.s.sol does (docs/LAUNCH.md walks through it).
  • A model is any contract with predict(int256[8]) pure returns (int256) and modelHash(). The repository trains one, quantises it to whole numbers and generates the Solidity from the weights file (research/), with golden vectors that the contract must reproduce exactly.
  • Pass share 0 and no Treasury if you do not want a share. The largest share the constructor accepts is 20%.

The fee a pool charges is whatever the policy returns, so check a new model the way this one is checked: Verify everything. The Treasury is at 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 here; it is deployed separately, with one buyback pool per fee currency.