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
| Contract | What it is | On Robinhood Chain |
|---|---|---|
| Hook | Sets the fee of every swap in its pools, and keeps each pool’s memory. Immutable, no owner. | 0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4 |
| Model | The neural network: a pure function from eight integers to one. Free to call. | 0xF5400cC2836D462605b5d7877b2C26F3f134E87e |
| Treasury | Receives the fee share and runs the buyback and burn, inside a price limit. | 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 |
| Price oracle | The 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 manager | Uniswap 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.
cast call 0xe98beA64f59d1Fd4C73df53B22C6439B601AB0c4 "previewReflex(bytes32)(int256[8],int256,uint24)" 0x7ba739294819cb9e217dfee21164dfcae213365b9efafb4680733a408666c846 --rpc-url https://rpc.mainnet.chain.robinhood.com
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.
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);
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.
| # | Input | What it measures |
|---|---|---|
| 0 | a_last | |Δtick| of the last observation, capped at 4096. 0 until there is one. |
| 1 | ewFast | Moving average of |Δtick| over observations, weight 1/4 each. Reacts within a few observations. |
| 2 | ewMid | The same average with weight 1/16. The rule-based baseline reads only this one. |
| 3 | ewSlow | The same average with weight 1/64: the long-run level of movement. |
| 4 | ewSigned | Moving average of the signed Δtick (weight 1/4): which way the price is leaning. |
| 5 | streak | Consecutive observations that moved the price the same way, from 0 to 16. |
| 6 | dtNow | Bits 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. |
| 7 | dtEwma | Moving average of the seconds between observations (weight 1/8). |
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.
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:
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’sHookMineragainst the CREATE2 deployer0x4e59…956C, ascontracts/script/DeployMyelin.s.soldoes (docs/LAUNCH.mdwalks through it). - A model is any contract with
predict(int256[8]) pure returns (int256)andmodelHash(). 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
0and 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.