Scaffold Placeholder. The surface described here is not built.
Create your own Mayflower market
Build a UI powered by Mayflower markets you create — each with a guaranteed floor, priced on a bonding curve. This guide is for UI builders who already have a provisioned tenant and want to stand up their own Mayflower market creation flow.
Topology and roles
Section titled “Topology and roles”Every EVM deployment is organized as:
Tenant → MarketGroup → Market| Role | Typical actor | What they do in this guide |
|---|---|---|
| Tenant admin | Your provisioned operator wallet | Creates market groups, names group admins |
| Group admin | Wallet you designate per group | Creates markets, raises the floor, collects group revenue |
| End user | Connected wallet in a consumer app | Covered in Build on a listed Mayflower market |
Floor raising is not permissionless. Only the active market-group admin may call raiseFloorFromExcessLiquidity or raiseFloorPreserveArea. Anyone may donateLiquidity, but donation alone does not raise the floor.
Install dependencies
Section titled “Install dependencies”npm install @mayflower-sys/evm-avm-sdk @mayflower-sys/evm-avm-interfaces-gen viempnpm add @mayflower-sys/evm-avm-sdk @mayflower-sys/evm-avm-interfaces-gen viemyarn add @mayflower-sys/evm-avm-sdk @mayflower-sys/evm-avm-interfaces-gen viembun add @mayflower-sys/evm-avm-sdk @mayflower-sys/evm-avm-interfaces-gen viemThe SDK and generated code package never touch the network — it packs reads and writes into { to, data } payloads you send with viem.
Connecting to the network
Section titled “Connecting to the network”Pick a network to get the RPC URL, chain id, and deployment addresses for the rest of this guide.
Overview
Run a preloaded anvil node from the demo package — a fully wired AVM with one tenant, ready for local iteration.
Prerequisites
- Foundry installed and
anvilon yourPATH
Install the demo package
From your project root:
npm install --save-dev @mayflower-sys/evm-avm-client-demo tsxpnpm add -D @mayflower-sys/evm-avm-client-demo tsxyarn add -D @mayflower-sys/evm-avm-client-demo tsxbun add -D @mayflower-sys/evm-avm-client-demo tsxStart anvil with the golden state
The demo ships a long-lived node script (no published CLI bin yet):
npx tsx node_modules/@mayflower-sys/evm-avm-client-demo/scripts/start-anvil.tspnpm exec tsx node_modules/@mayflower-sys/evm-avm-client-demo/scripts/start-anvil.tsyarn exec tsx node_modules/@mayflower-sys/evm-avm-client-demo/scripts/start-anvil.tsbun x tsx node_modules/@mayflower-sys/evm-avm-client-demo/scripts/start-anvil.tsWhen it starts you should see output like:
▶ anvil listening on http://localhost:8545 golden state loaded from …/fixtures/golden-state.json (Ctrl-C to stop)Chain id is 31337 (anvil default). Leave this terminal running while you work. Open a new terminal for following commands.
Read deployment addresses from the manifest
Contract addresses and the demo tenant live in a JSON file next to the golden state. Print it:
cat node_modules/@mayflower-sys/evm-avm-client-demo/fixtures/golden-manifest.json
Example output (yours may differ if the package version changed — always
trust your cat):
{ "network": { "rpcUrl": "http://localhost:8545", "chainName": "anvil", "protocolAdmin": "0x6b13585A90137dea5Ee0a674547a189Fb31D2F44" }, "contracts": { "marketImpl": "0xfd83eff0bd37fe07b5b8ead57969fa5f73f586d1", "directoryImpl": "0x8f3eaaae1acbf953ca715c63c02ec2f79eed4a27", "directoryProxy": "0xcbfefd94555b7676deeed155028ed39e5af1bc51", "marketFactory": "0xb0ddb061bf278ed0dbd968e7965bfb361b272a1a", "eventEmitterImpl": "0xde7fda308632a9bfd816c1b1cd4a0699ebcdb083", "eventEmitterProxy": "0x69df8e7159ecef6afab2f6c4727ec87ed0253c3b", "linearCurveEngineImpl": "0x02dde91ae43d9870f7678b190d1e66c79745feb1", "linearCurveEngineProxy": "0x0bc036eb0359a2b29b5a7c2bbc13bf58c0a0ccbd", "marketAdminActions": "0xbae2e9cf2f2dff09274de0376ad6f10250152a0b", "marketPositionActions": "0xefa4d8005bc3c3cb2182c9ed63edabee2597e29f", "marketTraderActions": "0xd1e35ed9be906e24ab2626ac25dfeac1a0034472" }, "tenant": { "id": "0xe8a091d995f061e21bb3127f20fc2c494e997e4de7719a44539353b2528b6ac1", "label": "golden-fixture-tenant", "admin": "0x681f9E19057e187B20c4592285e7b05705886A41", "adminPrivateKey": "0xe56d3c51cdc36cf9ad391fdc67508986e4c8b1a2cda0ab07b31211fbb0e5b651", "platformFeeMicroBps": "0" }} tenant.label is the human-readable name of your tenant. In
the golden fixture that label is golden-fixture-tenant.
Optional pretty-print if you have jq:
jq . node_modules/@mayflower-sys/evm-avm-client-demo/fixtures/golden-manifest.jsonMap JSON fields to your config
| Config field | Where to read it |
|---|---|
rpcUrl | Fixed http://localhost:8545 (also under network.rpcUrl) |
chainId | Fixed 31337 |
directoryAddress | contracts.directoryProxy |
marketFactoryAddress | contracts.marketFactory (create guide) |
tenantId | tenant.id (create guide) |
| Tenant label | tenant.label — e.g. golden-fixture-tenant |
| Tenant admin private key (local signing only) | tenant.adminPrivateKey |
You will copy many of these values into OperatorConfig in the
next section. The ones that you don’t copy over, like
tenant.adminPrivateKey, make sure to save/persist for future
use as well. Technically, the golden-manifest.json file in the
demo package will have everything saved, but be careful not to delete the
file if you don’t save elsewhere.
Hosted, testnet, and mainnet network entries will appear here when they are published. For now, use Local above to develop against anvil.
Configuration that your application holds
Section titled “Configuration that your application holds”Use the RPC URL, chain id, directory, and tenant id from Local above when filling OperatorConfig:
import type { Address, Hex } from "viem"
export interface OperatorConfig { /** Provided by the Mayflower team and will be specific to your chain. On local anvil, use `contracts.directoryProxy` from `golden-manifest.json`. */ directoryAddress: Address /** bytes32 tenant id assigned at provisioning. */ tenantId: Hex rpcUrl: string chainId: number}
/** Load operator settings from env (local scripts, CI, deploy targets). */export const operatorConfigFromEnv = (): OperatorConfig => { const directoryAddress = process.env.DIRECTORY_ADDRESS const tenantId = process.env.TENANT_ID const rpcUrl = process.env.RPC_URL ?? "http://localhost:8545" const chainId = Number(process.env.CHAIN_ID ?? "31337")
if (!directoryAddress || !tenantId) { throw new Error("missing DIRECTORY_ADDRESS or TENANT_ID") }
return { directoryAddress: directoryAddress as Address, tenantId: tenantId as Hex, rpcUrl, chainId, }}Transport helpers: wallet connection and RPC clients
Section titled “Transport helpers: wallet connection and RPC clients”Reuse a thin viem layer across your program. Helpful functions for sending payloads built by the SDKs to the RPC. Every snippet below builds on these:
import { createPublicClient, createWalletClient, http, type Address, type Hex, type PublicClient, type TransactionReceipt, type WalletClient, type Chain,} from "viem"import type { Account } from "viem/accounts"import type { PackedCall, PackedTx } from "@mayflower-sys/evm-avm-sdk"
export type BoundWallet = WalletClient< ReturnType<typeof http>, Chain, Account>
export const makeClients = (rpcUrl: string, chainId: number) => { const chain = { id: chainId, name: "mayflower-evm", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } }, } const transport = http(rpcUrl) return { publicClient: createPublicClient({ chain, transport }), walletFromAccount: (account: Account): BoundWallet => createWalletClient({ account, chain, transport }), }}
/** Run a packed eth_call and decode the typed result. */export const sendPackedCall = async <A>( publicClient: PublicClient, packed: PackedCall<A>,): Promise<A> => { const { data } = await publicClient.call({ to: packed.to, data: packed.data }) if (data === undefined) throw new Error("call returned no data") return packed.decode(data)}
/** Sign, send, and wait for a packed write. */export const sendPackedTx = async ( wallet: BoundWallet, publicClient: PublicClient, packed: PackedTx,): Promise<TransactionReceipt> => { const hash = await wallet.sendTransaction({ to: packed.to, data: packed.data, }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) if (receipt.status !== "success") { throw new Error(`transaction reverted (${hash})`) } return receipt}
/** Send a contract-creation tx and return the deployed address. */export const deployContract = async ( wallet: BoundWallet, publicClient: PublicClient, data: Hex,): Promise<Address> => { const hash = await wallet.sendTransaction({ data }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) if (receipt.status !== "success") { throw new Error(`deployment reverted (${hash})`) } if (!receipt.contractAddress) { throw new Error("deployment produced no contract address") } return receipt.contractAddress}Step 1 — Create a market group
Section titled “Step 1 — Create a market group”Signer: tenant admin. A market group is the container for your markets. It carries a fee schedule every market under it inherits.
Creating the market group is generally one-time. This is not something you would wire up to a UI, but rather execute through some kind of script.
Each script below writes its result to the terminal and appends the same output, with an execution timestamp, to scripts/logs/<script-name>.log. Add scripts/logs/ to .gitignore: the market-group log contains the generated group-admin private key.
scripts/logs/Group fees are MicroBps (100_000_000 = 100%). Each leg is capped at 10% on-chain.
import { Entities, Ix, Util } from "@mayflower-sys/evm-avm-sdk"import { appendFileSync, mkdirSync } from "node:fs"import type { Address, Hex, PublicClient } from "viem"import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"import { loadManifest } from "@mayflower-sys/evm-avm-client-demo/src/manifest.ts"
import { makeClients, sendPackedCall, sendPackedTx, type BoundWallet,} from "../src/lib/transport.ts"import type { OperatorConfig } from "../src/lib/config.ts"
type MarketGroupFees = { buy: bigint sell: bigint borrow: bigint exerciseOption: bigint}
type ScriptConfig = { directoryAddress: Address tenantId: Hex}
const MARKET_GROUP_FEES: MarketGroupFees = { buy: 10_000n, sell: 10_000n, borrow: 10_000n, exerciseOption: 10_000n,}
const logDirectory = new URL("./logs/", import.meta.url)const logFile = new URL("./logs/create-market-group.log", import.meta.url)
const log = (message: string): void => { process.stdout.write(message) mkdirSync(logDirectory, { recursive: true }) appendFileSync(logFile, `[${new Date().toISOString()}]\n${message}\n`)}
/** Read the group back for your admin dashboard. */export const readMarketGroup = ( publicClient: PublicClient, config: Pick<OperatorConfig, "directoryAddress">, marketGroupId: string,) => sendPackedCall( publicClient, Entities.MarketGroup.fetchMsg({ directoryAddress: config.directoryAddress, groupId: Util.strToBytes32(marketGroupId), }), )
const createMarketGroup = async ( config: ScriptConfig, tenantAdmin: BoundWallet, publicClient: PublicClient, params: { marketGroupId: string groupAdminAddress: Address fees: MarketGroupFees },): Promise<Hex> => { const groupId = Util.strToBytes32(params.marketGroupId)
const tx = Ix.CreateMarketGroup.pack({ directoryAddress: config.directoryAddress, data: { groupId, tenantId: config.tenantId, groupAdmin: params.groupAdminAddress, fees: params.fees, }, })
await sendPackedTx(tenantAdmin, publicClient, tx) return groupId}
const requiredEnv = (name: string): string => { const value = process.env[name] if (typeof value !== "string" || value.length === 0) { throw new Error(`Missing environment variable: ${name}`) } return value}
const rpcUrl = requiredEnv("RPC_URL")const chainId = Number(requiredEnv("CHAIN_ID"))const marketGroupId = process.env.MARKET_GROUP_ID ?? "demo-group"
const manifest = loadManifest()const { publicClient, walletFromAccount } = makeClients(rpcUrl, chainId)const tenantAdmin = walletFromAccount( privateKeyToAccount(manifest.tenantAdminPrivateKey),)const groupAdminPrivateKey = generatePrivateKey()const groupAdmin = privateKeyToAccount(groupAdminPrivateKey)
const config = { directoryAddress: manifest.directoryAddress, tenantId: manifest.tenantId, rpcUrl, chainId,}
const replacer = (_key: string, value: unknown) => typeof value === "bigint" ? value.toString() : value
const groupId = await createMarketGroup(config, tenantAdmin, publicClient, { marketGroupId, groupAdminAddress: groupAdmin.address, fees: MARKET_GROUP_FEES,})
const group = await readMarketGroup(publicClient, config, marketGroupId)
log( `Market group created\n` + ` marketGroupId: ${marketGroupId}\n` + ` groupId: ${groupId}\n` + ` groupAdmin: ${groupAdmin.address}\n` + ` groupAdminPrivateKey: ${groupAdminPrivateKey}\n\n` + `${JSON.stringify(group, replacer, 2)}\n`,)Run from your project root:
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_GROUP_ID=my-group-1 npx tsx ./scripts/create-market-group.tsTypical launchpad fee starting point (0.01% per leg — tune for your product):
export const MARKET_GROUP_FEES: MarketGroupFees = { buy: 10_000n, // remember this is MicroBps, so this comes out to 0.01% sell: 10_000n, borrow: 10_000n, exerciseOption: 10_000n,}Fund the group admin
Section titled “Fund the group admin”Step 1 prints a new group admin address and private key. Steps 3 and later sign with that wallet. Before you continue, fund it with native ETH for create-market and other admin writes.
On testnet or mainnet, transfer native ETH to the groupAdmin address from the Step 1 script output.
On Local anvil, the generated group admin starts with zero ETH. Use Foundry cast to set its balance before Step 3:
ADDR=0xYourGroupAdminAddressRPC=http://localhost:8545WEI=0x56bc75e2d63100000
echo "Before:"cast balance "$ADDR" --rpc-url "$RPC"
cast rpc anvil_setBalance "$ADDR" "$WEI" --rpc-url "$RPC"
echo "After:"cast balance "$ADDR" --rpc-url "$RPC" --etherReplace ADDR with groupAdmin from the Step 1 script output. WEI is 100 ETH (0x56bc75e2d63100000).
Step 2 — Choose a reserve asset
Section titled “Step 2 — Choose a reserve asset”Every market prices against one ERC-20 reserve token. Use an existing token your users already hold, or deploy one.
Pass its address to the Step 3 script as RESERVE_TOKEN_ADDRESS. When you create a market, it will inherit the reserve token’s decimals for its AVM and option tokens.
Step 3 — Create a market
Section titled “Step 3 — Create a market”Signer: group admin. Mayflower market creation is likely something you only do via an admin UI, or through scripts — since the signer must be the market group admin. As markets are created, you then expose them in your UI for further interaction.
EVM currently ships one pricing engine end-to-end: the linear curve. You configure five curve parameters at creation:
| Parameter | Meaning |
|---|---|
slope | How fast marginal price rises with supply on the main segment. |
floor | Starting guaranteed redemption price (reserve per AVM token). |
rampScalar | Shoulder steepness multiplier (> 1; shoulder slope = scalar × slope). |
rampEndSupply | Supply coordinate at the end of the ramp shoulder (x₂); on-chain rampEndSupply. |
rampWidth | Width of the ramp shoulder (x₂ − x₁); must be > 0. |
Both rampEndSupply and rampWidth must be positive, and rampEndSupply must exceed rampWidth — a zero-width ramp or negative x₁ causes the engine to reject later floor raises. Step 4’s read-market-state.ts reports the floor–ramp junction x₁ = x₂ − width as rampStart for display.
For the math behind floor, ramp, and main, see The Assured Value Machine.
import { encodeLinearCurveState, IAvmFactory, type MarketFlags,} from "@mayflower-sys/evm-avm-interfaces-gen"import { Events, Fixed18, LINEAR_CURVE_MARKET_KIND, Util,} from "@mayflower-sys/evm-avm-sdk"import { loadManifest } from "@mayflower-sys/evm-avm-client-demo/src/manifest.ts"import { appendFileSync, mkdirSync } from "node:fs"import { type Address, type Hex, type PublicClient } from "viem"import { privateKeyToAccount } from "viem/accounts"
import { makeClients, sendPackedTx, type BoundWallet,} from "../src/lib/transport.ts"
export interface CreatedMarket { marketAddress: Address avmToken: Address optionToken: Address reserveToken: Address marketId: Hex groupId: Hex}
type ScriptConfig = { marketFactoryAddress: Address}
/** Human-readable linear curve. `rampEndSupply` is x₂ (on-chain ramp end); * `rampWidth` is x₂ − x₁. Both must be > 0 and rampEndSupply > rampWidth. */type LinearCurveParams = { slope: number floor: number rampScalar: number rampEndSupply: number rampWidth: number}
const logDirectory = new URL("./logs/", import.meta.url)const logFile = new URL("./logs/create-market.log", import.meta.url)
const log = (message: string): void => { process.stdout.write(message) mkdirSync(logDirectory, { recursive: true }) appendFileSync(logFile, `[${new Date().toISOString()}]\n${message}\n`)}
/** Group admin creates a linear Mayflower market. */const createLinearMarket = async ( config: ScriptConfig, groupAdmin: BoundWallet, publicClient: PublicClient, params: { marketGroupId: string marketId: string reserveTokenAddress: Address avmTokenName: string avmTokenSymbol: string optionTokenName: string optionTokenSymbol: string } & LinearCurveParams,): Promise<CreatedMarket> => { const groupId = Util.strToBytes32(params.marketGroupId) const marketId = Util.strToBytes32(params.marketId) if (!(params.rampEndSupply > params.rampWidth) || !(params.rampWidth > 0)) { throw new Error( "RAMP_END_SUPPLY must exceed RAMP_WIDTH and RAMP_WIDTH must be > 0 (the engine rejects floor raises on a zero-width ramp)", ) } if (!(params.rampScalar > 1)) { throw new Error( "RAMP_SCALAR must be > 1 (shoulder slope = scalar × main slope)", ) }
const flags: MarketFlags = { canBuy: true, canSell: true, canBorrowReserve: true, canRepayReserve: true, canDepositAvm: true, canWithdrawAvm: true, canExerciseOption: true, canDonateLiquidity: true, } const data = IAvmFactory.encodeCreateMarket({ kind: LINEAR_CURVE_MARKET_KIND, groupId, marketId, reserveToken: params.reserveTokenAddress, engineInitData: encodeLinearCurveState({ schemaVersion: 1, floorPrice: Fixed18.fromNumber(params.floor), rampEndSupply: Fixed18.fromNumber(params.rampEndSupply), rampWidth: Fixed18.fromNumber(params.rampWidth), rampScalar: Fixed18.fromNumber(params.rampScalar), slope: Fixed18.fromNumber(params.slope), }), flags, dutchAuctionConfig: { initBoost: 0n, duration: 0, curvature: 0n, }, avmTokenName: params.avmTokenName, avmTokenSymbol: params.avmTokenSymbol, optionTokenName: params.optionTokenName, optionTokenSymbol: params.optionTokenSymbol, })
const receipt = await sendPackedTx(groupAdmin, publicClient, { to: config.marketFactoryAddress, data, }) const created = Events.findEvent(receipt, "MarketCreated")
return { marketAddress: created.args.marketAddress, avmToken: created.args.tokens.avmToken, optionToken: created.args.tokens.optionToken, reserveToken: params.reserveTokenAddress, marketId, groupId, }}
const requiredEnv = (...names: string[]): string => { for (const name of names) { const value = process.env[name] if (typeof value === "string" && value.length > 0) { return value } } throw new Error(`Missing environment variable: ${names.join(" or ")}`)}
const envNumber = (name: string, fallback: number): number => { const value = process.env[name] if (typeof value !== "string" || value.length === 0) { return fallback } const parsed = Number(value) if (!Number.isFinite(parsed)) { throw new Error(`${name} must be a number (got ${value})`) } return parsed}
const replacer = (_key: string, value: unknown) => typeof value === "bigint" ? value.toString() : value
const rpcUrl = requiredEnv("RPC_URL")const chainId = Number(requiredEnv("CHAIN_ID"))const reserveTokenAddress = requiredEnv("RESERVE_TOKEN_ADDRESS") as Addressconst marketGroupId = process.env.MARKET_GROUP_ID ?? "demo-group"const marketId = process.env.MARKET_ID ?? "market-1"const curve: LinearCurveParams = { slope: envNumber("SLOPE", 0.0001), floor: envNumber("FLOOR", 1), rampScalar: envNumber("RAMP_SCALAR", 2), rampEndSupply: envNumber("RAMP_END_SUPPLY", 60), rampWidth: envNumber("RAMP_WIDTH", 20),}const groupAdminPrivateKey = requiredEnv("GROUP_ADMIN_PRIVATE_KEY") as Hex
const manifest = loadManifest()const { publicClient, walletFromAccount } = makeClients(rpcUrl, chainId)const groupAdminAccount = privateKeyToAccount(groupAdminPrivateKey)const groupAdmin = walletFromAccount(groupAdminAccount)
const created = await createLinearMarket( { marketFactoryAddress: manifest.marketFactoryAddress }, groupAdmin, publicClient, { marketGroupId, marketId, reserveTokenAddress, avmTokenName: "AVM", avmTokenSymbol: "AVM", optionTokenName: "OPTION", optionTokenSymbol: "OPTION", ...curve, },)
log( `Market created\n` + ` marketGroupId: ${marketGroupId}\n` + ` marketId: ${marketId}\n` + ` groupId: ${created.groupId}\n` + ` marketIdBytes32: ${created.marketId}\n` + ` marketAddress: ${created.marketAddress}\n` + ` avmToken: ${created.avmToken}\n` + ` optionToken: ${created.optionToken}\n` + ` reserveToken: ${created.reserveToken}\n` + ` slope: ${curve.slope}\n` + ` floor: ${curve.floor}\n` + ` rampScalar: ${curve.rampScalar}\n` + ` rampEndSupply: ${curve.rampEndSupply}\n` + ` rampWidth: ${curve.rampWidth}\n\n` + `${JSON.stringify(created, replacer, 2)}\n`,)Run from your project root:
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_GROUP_ID=my-group-1 GROUP_ADMIN_PRIVATE_KEY=0xabc... RESERVE_TOKEN_ADDRESS=0xdef... npx tsx ./scripts/create-market.tsCurve defaults (SLOPE, FLOOR, RAMP_SCALAR, RAMP_END_SUPPLY, RAMP_WIDTH) apply when omitted — tune them for your product.
If you continue to Build on a listed Mayflower market after you complete this guide, paste marketAddress from the script output into App.tsx as MARKET_ADDRESS. Reserve, AVM, and option token addresses and decimals are read from on-chain state — you do not need a separate market config module.
Step 4 adds read-market-state.ts so you can hydrate spot price, segment, and flags from that address.
Step 4 — Read market state for rendering
Section titled “Step 4 — Read market state for rendering”After a market is live, your application needs a single read path that returns everything you would potentially want to show:
current prices, supply, reserve balance, curve parameters, operation flags, and which segment the market is on (floor, ramp, or main).
The helper below batches three reads — snapshot() for live market stats, state() for curve shape,
and netIssuedSupply() for segment detection — into one typed object you can bind directly to UI components.
The same helper is reused in Build on a listed Mayflower market for quotes and end-user flows.
The below code assumes a linear market engine.
import { Entities, Fixed18 } from "@mayflower-sys/evm-avm-sdk"import { IMarketViews } from "@mayflower-sys/evm-avm-interfaces-gen"import type { Address, PublicClient } from "viem"import { sendPackedCall } from "./transport"
export type CurveSegment = "floor" | "ramp" | "main"
export interface MarketState { spotPrice: string floorPrice: string reserveBalance: bigint avmSupply: bigint canBuy: boolean canSell: boolean canBorrow: boolean decimals: number /** Full decoded market — pass to `buildLinearMarketCalculator`. */ market: Entities.Market.Market netIssuedSupply: bigint slope: string rampStart: string rampWidth: string rampScalar: string segment: CurveSegment}
const segmentAtSupply = ( supply: Fixed18.Fixed18, rampStart: Fixed18.Fixed18, rampEnd: Fixed18.Fixed18,): CurveSegment => { const x = Fixed18.toRaw(supply) const x1 = Fixed18.toRaw(rampStart) const x2 = Fixed18.toRaw(rampEnd) if (x <= x1) return "floor" if (x < x2) return "ramp" return "main"}
/** Hydrate the numbers your UI needs to render market state. */export const readMarketState = async ( publicClient: PublicClient, marketAddress: Address,): Promise<MarketState> => { const [snapshotResult, market, netIssuedSupply] = await Promise.all([ publicClient.call({ to: marketAddress, data: IMarketViews.encodeSnapshot(), }), sendPackedCall(publicClient, Entities.Market.fetchMsg(marketAddress)), sendPackedCall(publicClient, { to: marketAddress, data: IMarketViews.encodeNetIssuedSupply(), decode: IMarketViews.decodeNetIssuedSupply, }), ]) if (snapshotResult.data === undefined) { throw new Error("snapshot returned no data") } if (market.engine._tag !== "linear") { throw new Error(`unsupported engine: ${market.engine._tag}`) }
const snap = IMarketViews.decodeSnapshot(snapshotResult.data) const { segmentation, characteristic } = market.engine const supply = Fixed18.fromToken(netIssuedSupply, market.decimals) // SDK field `segmentation.rampStart` is the on-chain ramp-end coordinate x₂. const rampEnd = segmentation.rampStart const rampStart = Fixed18.fromRaw( Fixed18.toRaw(rampEnd) - Fixed18.toRaw(segmentation.rampWidth), )
return { spotPrice: Fixed18.toString(Fixed18.fromRaw(snap.avmPriceInReserveToken)), floorPrice: Fixed18.toString( Fixed18.fromRaw(snap.floorPriceInReserveToken), ), reserveBalance: snap.reserveTokenBalance, avmSupply: snap.avmTokenSupply, canBuy: snap.flags.canBuy, canSell: snap.flags.canSell, canBorrow: snap.flags.canBorrowReserve, decimals: market.decimals, market, netIssuedSupply, slope: Fixed18.toString(characteristic.slope), rampStart: Fixed18.toString(rampStart), rampWidth: Fixed18.toString(segmentation.rampWidth), rampScalar: Fixed18.toString(segmentation.rampScalar), segment: segmentAtSupply(supply, rampStart, rampEnd), }}import { useEffect, useState } from "react"import type { Address, Hex } from "viem"
import type { OperatorConfig } from "./lib/config"import { readMarketState, type MarketState } from "./lib/read-market-state"import { makeClients } from "./lib/transport"
/** Paste rpcUrl / chainId from Local setup; directory and tenant from provisioning. */const operator: OperatorConfig = { rpcUrl: "http://localhost:8545", chainId: 31337, directoryAddress: "0xYourDirectoryProxy" as Address, tenantId: "0xYourTenantId" as Hex,}
/** Paste the market address logged by scripts/create-market.ts. */const MARKET_ADDRESS = "0xYourMarketAddress" as Address
export default function App() { const [state, setState] = useState<MarketState | null>(null) const [error, setError] = useState<string | null>(null) const [loading, setLoading] = useState(true)
useEffect(() => { let cancelled = false
async function loadMarketState() { try { const { publicClient } = makeClients(operator.rpcUrl, operator.chainId) const next = await readMarketState(publicClient, MARKET_ADDRESS) if (!cancelled) setState(next) } catch (cause) { if (!cancelled) { setError(cause instanceof Error ? cause.message : String(cause)) } } finally { if (!cancelled) setLoading(false) } }
loadMarketState()
return () => { cancelled = true } }, [])
if (loading) { return ( <MarketPage> <p className="market-muted">Loading market state…</p> </MarketPage> ) }
if (error || !state) { return ( <MarketPage> <p className="market-muted">Could not load market state.</p> {error ? <pre className="market-panel market-panel--error">{error}</pre> : null} </MarketPage> ) }
return ( <MarketPage> <p className="market-subtitle"> Live state for <code className="market-code">{MARKET_ADDRESS}</code>. </p> <MarketOverview state={state} /> </MarketPage> )}
function MarketPage({ children }: { children: React.ReactNode }) { return ( <> <MarketStyles /> <main className="market-page"> <div className="market-page__inner"> <h1 className="market-title">Mayflower Market</h1> {children} </div> </main> </> )}
function MarketOverview({ state }: { state: MarketState }) { const priceRows = [ { label: "Spot", value: state.spotPrice }, { label: "Floor", value: state.floorPrice, emphasis: true }, { label: "Segment", value: state.segment }, ]
const supplyRows = [ { label: "AVM supply", value: String(state.avmSupply) }, { label: "Net issued", value: String(state.netIssuedSupply) }, { label: "Reserve balance", value: String(state.reserveBalance) }, { label: "Decimals", value: String(state.decimals) }, ]
const curveRows = [ { label: "Slope", value: state.slope, emphasis: true }, { label: "Ramp start", value: state.rampStart }, { label: "Ramp width", value: state.rampWidth }, { label: "Ramp scalar", value: state.rampScalar }, ]
const availabilityItems = [ { label: "Can buy", enabled: state.canBuy }, { label: "Can sell", enabled: state.canSell }, { label: "Can borrow", enabled: state.canBorrow }, ]
return ( <div className="market-grid"> <DataCard title="Prices"> <DetailRows rows={priceRows} /> </DataCard>
<DataCard title="Availability"> <dl> {availabilityItems.map(({ label, enabled }) => ( <div key={label} className="market-detail-row"> <dt>{label}</dt> <dd> <AvailabilityBadge enabled={enabled} /> </dd> </div> ))} </dl> </DataCard>
<DataCard title="Supply and reserve"> <DetailRows rows={supplyRows} /> </DataCard>
<DataCard title="Linear curve"> <DetailRows rows={curveRows} /> </DataCard> </div> )}
function DataCard({ title, children,}: { title: string children: React.ReactNode}) { const headingId = `${title.replace(/\s+/g, "-").toLowerCase()}-heading`
return ( <section className="market-card" aria-labelledby={headingId}> <h2 id={headingId} className="market-card__title"> {title} </h2> <div>{children}</div> </section> )}
type DetailRow = { label: string value: string emphasis?: boolean}
function DetailRows({ rows }: { rows: DetailRow[] }) { return ( <dl> {rows.map((row) => ( <div key={row.label} className="market-detail-row"> <dt>{row.label}</dt> <dd className={ row.emphasis ? "market-detail-row__value--emphasis" : undefined } > {row.value} </dd> </div> ))} </dl> )}
function AvailabilityBadge({ enabled }: { enabled: boolean }) { return ( <span className={ enabled ? "market-badge market-badge--yes" : "market-badge market-badge--no" } > {enabled ? "Yes" : "No"} </span> )}
function MarketStyles() { return ( <style>{` .market-page { min-height: 100vh; width: 100%; background: #eef1f6; color: #1e293b; font-family: Inter, ui-sans-serif, system-ui, sans-serif; line-height: 1.5; }
.market-page__inner { max-width: 720px; margin: 0 auto; padding: 2.5rem 1.25rem; }
@media (min-width: 640px) { .market-page__inner { padding: 3.5rem 2rem; } }
.market-title { margin: 0 0 1.5rem; font-family: Fraunces, ui-serif, Georgia, serif; font-size: 1.75rem; font-weight: 600; letter-spacing: -0.02em; }
@media (min-width: 640px) { .market-title { font-size: 1.875rem; } }
.market-subtitle { margin: 0 0 1.5rem; color: #64748b; font-size: 0.9375rem; }
.market-muted { margin: 0; color: #64748b; }
.market-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8125rem; word-break: break-all; }
.market-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 640px) { .market-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.market-card { border: 1px solid #e2e8f0; border-radius: 2px; background: #ffffff; padding: 1.25rem; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); }
.market-card__title { margin: 0 0 0.75rem; font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: #64748b; }
.market-detail-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.625rem 0; border-bottom: 1px solid #e2e8f0; }
.market-detail-row:first-child { padding-top: 0; }
.market-detail-row:last-child { border-bottom: none; padding-bottom: 0; }
.market-detail-row dt { font-size: 0.875rem; color: #64748b; }
.market-detail-row dd { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.875rem; font-weight: 500; text-align: right; }
.market-detail-row__value--emphasis { color: #db2777; font-weight: 600; }
.market-badge { display: inline-flex; border-radius: 2px; padding: 0.125rem 0.5rem; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 600; }
.market-badge--yes { background: rgba(219, 39, 119, 0.1); color: #db2777; }
.market-badge--no { background: #f1f5f9; color: #64748b; }
.market-panel { margin-top: 1rem; border-radius: 2px; padding: 0.75rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.875rem; overflow-x: auto; }
.market-panel--error { border: 1px solid #fecaca; background: #fef2f2; color: #b91c1c; } `}</style> )}In a full-fledged UI, you’ll want to automatically update this state after certain actions — such as issuance, redemption, cash advances, floor raises, and donations of liquidity. These actions mutate the market state.
Step 5 — Raise the floor
Section titled “Step 5 — Raise the floor”Signer: group admin only. To unlock the full value of Mayflower markets, you must raise the floor. Raising the floor is the mechanism by which Mayflower assets build value on a level that is revolutionary compared to traditional financial assets. Two mechanisms for floor-raising exist on-chain:
| Method | When to use |
|---|---|
raiseFloorFromExcessLiquidity | Market holds surplus reserve above the curve area (e.g. after buys or donateLiquidity) |
raiseFloorPreserveArea | Supply is on the main segment; you choose a new ramp-end supply explicitly |
Both mechanisms require a strictly increasing floor price. Donations are permissionless but do not raise the floor by themselves — the group admin must call a raise. Much like market creation, this is not a mechanism for end-users, but something internal. It often makes sense for floor raising to be managed programmatically - like by a bot. It could also be very sensible to create an admin dashboard capable of sending floor-raise transactions.
This guide scripts the excess-liquidity path. raiseFloorPreserveArea instead rearranges the existing curve without adding reserve: while supply is on the main segment, the group admin selects a higher floor and a new ramp-end supply no greater than current supply. The market preserves reserve, spot price, and the main schedule while converting more of the curve into guaranteed floor value. See the interface reference for argument units and ordering.
raiseFloorFromExcessLiquidity needs surplus reserve already sitting in the market. The scripts below walk through creating that surplus (buy + donate), then consuming it in a floor raise.
Before you can raise the floor, you’ll need to have issued some shares and donated some excess liquidity. If your market is brand new and you have only followed the steps in this guide, this is needed.
The below script handles that for you. It is designed for local development, so will need to be tweaked for testnet.
It spends reserve held by the group admin, issues a fixed AVM amount, and calls donateLiquidity — leaving surplus the floor-raise script can consume.
Before running the script, mint mock reserve to the group admin:
RESERVE=0xYourReserveTokenTO=0xYourGroupAdminAddressRPC=http://localhost:8545# 1,000,000 RESERVE @ 6 decimalsAMOUNT=1000000000000# Anvil default account #0 — mock ERC-20 mint is unrestricted on the local fixtureKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cast send "$RESERVE" "mint(address,uint256)" "$TO" "$AMOUNT" --rpc-url "$RPC" --private-key "$KEY"Replace RESERVE with your Step 2 deploy address and TO with groupAdmin from the Step 1 script output.
import { IMarket } from "@mayflower-sys/evm-avm-interfaces-gen"import { Entities, Ix } from "@mayflower-sys/evm-avm-sdk"import { mockErc20 } from "@mayflower-sys/evm-avm-client-demo/fixtures/mock-erc20.ts"import { appendFileSync, mkdirSync } from "node:fs"import { encodeFunctionData, parseUnits, type Address, type Hex } from "viem"import { privateKeyToAccount } from "viem/accounts"
import { makeClients, sendPackedCall, sendPackedTx,} from "../src/lib/transport.ts"
const logDirectory = new URL("./logs/", import.meta.url)const logFile = new URL("./logs/buy-and-donate-local.log", import.meta.url)
const log = (message: string): void => { process.stdout.write(message) mkdirSync(logDirectory, { recursive: true }) appendFileSync(logFile, `[${new Date().toISOString()}]\n${message}\n`)}
const requiredEnv = (name: string): string => { const value = process.env[name] if (typeof value !== "string" || value.length === 0) { throw new Error(`Missing environment variable: ${name}`) } return value}
const rpcUrl = requiredEnv("RPC_URL")const chainId = Number(requiredEnv("CHAIN_ID"))const marketAddress = requiredEnv("MARKET_ADDRESS") as Addressconst sharesHuman = process.env.BUY_SHARES ?? "100"const donateHuman = process.env.DONATE_AMOUNT ?? "1000"
const { publicClient, walletFromAccount } = makeClients(rpcUrl, chainId)const wallet = walletFromAccount( privateKeyToAccount(requiredEnv("GROUP_ADMIN_PRIVATE_KEY") as Hex),)
const market = await sendPackedCall( publicClient, Entities.Market.fetchMsg(marketAddress),)const reserveToken = market.reserveTokenAddressconst sharesOut = parseUnits(sharesHuman, market.decimals)const donateAmount = parseUnits(donateHuman, market.decimals)const maxReserveIn = parseUnits("1000000", market.decimals)const approvalAmount = maxReserveIn + donateAmount
const send = async (to: Address, data: Hex) => { const hash = await wallet.sendTransaction({ to, data }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) if (receipt.status !== "success") { throw new Error(`tx reverted (${hash})`) }}
await send( reserveToken, encodeFunctionData({ abi: mockErc20.abi, // only approve with this ABI on local. You will need actual ERC-20 reserve abi on EVM testnets functionName: "approve", args: [marketAddress, approvalAmount], }),)
await sendPackedTx( wallet, publicClient, Ix.IssueSharesWithExactSharesOut.pack({ market: marketAddress, exactSharesOut: sharesOut, maxReserveIn, receiver: wallet.account.address, }),)
await send(marketAddress, IMarket.encodeDonateLiquidity(donateAmount))
log( `bought ${sharesHuman} AVM and donated ${donateHuman} RESERVE\n` + ` market: ${marketAddress}\n` + ` reserveToken: ${reserveToken}\n`,)Run from your project root:
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_ADDRESS=0xabc... GROUP_ADMIN_PRIVATE_KEY=0xabc... npx tsx ./scripts/buy-and-donate-local.tsOptional overrides: BUY_SHARES (default 100) and DONATE_AMOUNT (default 1000).
Once surplus exists in the market, the group admin calls raiseFloorFromExcessLiquidity:
import { IMarket } from "@mayflower-sys/evm-avm-interfaces-gen"import { Fixed18 } from "@mayflower-sys/evm-avm-sdk"import { appendFileSync, mkdirSync } from "node:fs"import type { Address, PublicClient } from "viem"import { privateKeyToAccount } from "viem/accounts"
import { makeClients, type BoundWallet } from "../src/lib/transport.ts"
const logDirectory = new URL("./logs/", import.meta.url)const logFile = new URL("./logs/raise-floor.log", import.meta.url)
const log = (message: string): void => { process.stdout.write(message) mkdirSync(logDirectory, { recursive: true }) appendFileSync(logFile, `[${new Date().toISOString()}]\n${message}\n`)}
/** Group admin raises floor from surplus liquidity already in the market. */export const raiseFloorFromSurplus = async ( groupAdmin: BoundWallet, publicClient: PublicClient, marketAddress: Address, newFloorHuman: number,) => { const newFloorPrice = Fixed18.fromNumber(newFloorHuman) // ExchangeRate = 18-decimal reserve per AVM
const data = IMarket.encodeRaiseFloorFromExcessLiquidity(newFloorPrice)
const hash = await groupAdmin.sendTransaction({ to: marketAddress, data }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) if (receipt.status !== "success") { throw new Error(`floor raise reverted (${hash})`) } return receipt}
const requiredEnv = (name: string): string => { const value = process.env[name] if (typeof value !== "string" || value.length === 0) { throw new Error(`Missing environment variable: ${name}`) } return value}
const rpcUrl = requiredEnv("RPC_URL")const chainId = Number(requiredEnv("CHAIN_ID"))const marketAddress = requiredEnv("MARKET_ADDRESS") as Addressconst newFloorHuman = Number(requiredEnv("NEW_FLOOR"))const { publicClient, walletFromAccount } = makeClients(rpcUrl, chainId)const groupAdmin = walletFromAccount( privateKeyToAccount(requiredEnv("GROUP_ADMIN_PRIVATE_KEY") as `0x${string}`),)
await raiseFloorFromSurplus( groupAdmin, publicClient, marketAddress, newFloorHuman,)
log(`floor raised to ${newFloorHuman}\n`)Run from your project root:
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_ADDRESS=0xabc... GROUP_ADMIN_PRIVATE_KEY=0xabc... NEW_FLOOR=1.0001 npx tsx ./scripts/raise-floor.tsAfter confirmation, refresh the app that we created in Step 4. You will see the new floor and new reserve balances. The floor is monotonic — it never decreases.
What you should have now
Section titled “What you should have now”After this guide you can:
- Open a market group under your tenant
- Create a linear Mayflower market with your chosen reserve token and curve params
- Display curve and reserve state in a TypeScript frontend
- Buy, donate, and raise the floor — create surplus reserve locally, then raise the floor as group admin
Next steps
Section titled “Next steps”With the market created and the market address persisted, it’s time to interact with your newly listed Mayflower market. Consider reading Build on a listed Mayflower market to learn how you can build issuance, redemption, and cash-advance functionality on this new market.