Tokenize a pools.trade fee NFT
From /tokenize or a script: approve the tokenizer, then tokenizeFeeb with the terms and a market; rHOOKR's fee NFT went in through tokenizeStreams.
On this page
A pools.trade fee NFT goes into a revenue token in one call, and the token's income is the ETH the NFT is paid. rHOOKR, the second revenue token the tokenizer made, holds one: the fee NFT of pools.trade's ETH/HOOKR pool, with a market of its own.
In the app
- Claim what is pending on pools.trade first: what is still pending when the fee NFT moves becomes the revenue token's income.
- Open /tokenize with the wallet that holds the fee NFT. Your wallet lists it with its pair: tick it, press Tokenize, set the terms and sign, as Tokenize a fee right you already hold shows. This tab sends the fee NFT as one stream of
tokenizeStreams, the way rHOOKR was made. - Or open By id and its pools.trade fee NFT card: type the vault and the fee NFT's id, press Read the fee NFT, set the terms, and press Approve and tokenize. This card sends
tokenizeFeeb.
Either way the token gets one fee NFT door. Only the record differs: tokenizeFeeb records way 2, and tokenizeStreams records way 5 and adds one StreamAdded per stream.
The calls
approve(tokenizer, tokenId)on the vault, unlessgetApproved(tokenId)already names the tokenizer. The vault must be one the tokenizer admits, asadmittedVaults()lists them.tokenizeFeeb(vault, tokenId, t, l, m). In that one call the tokenizer deploys the token, makes a fee NFT door for it and moves the NFT into the door for good, opens the market whenm.open, and mints the whole supply, the market's seed included. The value sent is the launcher'slaunchFee()with a market and nothing without one.- The receipt's
Tokenizednames the token. Its fee NFT door isdoorAt(0).
rHOOKR's mint fixed these, read from the chain:
- a supply of 1,000,000,000: 5,000,000 to its market's seed and 945,000,000 to the creator;
- a release period of 1 day and a tip of 1%;
- ETH as its only asset;
- three doors, each for good: the fee NFT door (gas stipend 300,000), the Rules ledger its market's royalty is paid through (250,000), and the family door holding its market (600,000).
In code
With client.ts and abis.ts:
import { createWalletClient, http, isAddressEqual, keccak256, parseEventLogs, parseUnits, toHex, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";
import { client, FOREVER, TOKENIZER } from "./client";
import { feeNftVaultAbi, launchFeeAbi, tokenizerAbi } from "./abis";
const creator = privateKeyToAccount(process.env.CREATOR_KEY as `0x${string}`);
const wallet = createWalletClient({ account: creator, chain: robinhood, transport: http() });
const VAULT: Address = "0x…"; // the pools.trade vault that holds the fee NFT
const FEE_NFT = 123456n; // your fee NFT's id
// 1. Let the tokenizer move the fee NFT.
const approved = await client.readContract({ address: VAULT, abi: feeNftVaultAbi, functionName: "getApproved", args: [FEE_NFT] });
if (!isAddressEqual(approved, TOKENIZER)) {
const hash = await wallet.writeContract({ address: VAULT, abi: feeNftVaultAbi, functionName: "approve", args: [TOKENIZER, FEE_NFT] });
await client.waitForTransactionReceipt({ hash });
}
// 2. The terms. A fee NFT pays Hookr nothing on its own, so the mint carries Hookr's outside leg.
const supply = parseUnits("1000000000", 18);
const seed = parseUnits("5000000", 18); // the market's seed, out of the creator's side
const legBps = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "HOOKR_LEG_BPS_OUTSIDE" });
const leg = (supply * BigInt(legBps)) / 10_000n;
const terms = {
name: "Example Revenue",
symbol: "rEXAMPLE",
supply,
streamSeconds: 86_400, // the release period: 1 day
tipBps: 100, // the sync caller's tip: 1%
extraAssets: [], // a fee NFT pays ETH only
recipients: [creator.address],
amounts: [supply - leg - seed],
termsHash: keccak256(toHex("rEXAMPLE: 1,000,000,000 supply, 1-day release period, 1% tip")),
};
// 3. The market at mint, on the tokenizer's own terms. The start price is revenue tokens per ETH as a sqrt price:
// here 1 ETH buys 1,000,000 revenue tokens.
const sqrt = (n: bigint) => {
let x = n;
let y = (x + 1n) / 2n;
while (y < x) [x, y] = [y, (y + n / y) / 2n];
return x;
};
const market = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "defaultMarket", args: [sqrt(1_000_000n << 192n), seed] });
const launcher = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "launcher" });
const [launchFee] = await client.readContract({ address: launcher, abi: launchFeeAbi, functionName: "launchFee" });
// 4. Lock and mint in one call. The market's family is locked for good.
const { request } = await client.simulateContract({
account: creator,
address: TOKENIZER,
abi: tokenizerAbi,
functionName: "tokenizeFeeb",
args: [VAULT, FEE_NFT, terms, { unlockBlock: FOREVER }, market],
value: launchFee,
});
const receipt = await client.waitForTransactionReceipt({ hash: await wallet.writeContract(request) });
const [minted] = parseEventLogs({ abi: tokenizerAbi, eventName: "Tokenized", logs: receipt.logs });
console.log("revenue token", minted.args.token, "Hookr's leg", minted.args.legBps, "bps");A fee NFT pays ETH, so extraAssets stays empty. Without a market, send { ...market, open: false } and no value: the tokenizer reads no other field of a closed market. The seed comes out of the creator's side.
After the mint
The fee NFT's ETH reaches the vault only when collectFees runs on pools.trade's fee splitter, so the token page's Sync calls it first, and so should a script: Keepers and bots.
Fee
A fee NFT pays Hookr nothing through the Hookr hook, so the mint gives Hookr's leg, 5% of the supply, to the leg collector: rHOOKR's mint sent it 50,000,000. The recipients' amounts are the supply less the market's seed and that leg, exactly, as the code above works out.