Skip to content

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.

Every EVM deployment is organized as:

Tenant → MarketGroup → Market
RoleTypical actorWhat they do in this guide
Tenant adminYour provisioned operator walletCreates market groups, names group admins
Group adminWallet you designate per groupCreates markets, raises the floor, collects group revenue
End userConnected wallet in a consumer appCovered 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.

Terminal window
npm install @mayflower-sys/evm-avm-sdk @mayflower-sys/evm-avm-interfaces-gen viem

The SDK and generated code package never touch the network — it packs reads and writes into { to, data } payloads you send with viem.

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 anvil on your PATH

Install the demo package

From your project root:

Terminal window
npm install --save-dev @mayflower-sys/evm-avm-client-demo tsx

Start anvil with the golden state

The demo ships a long-lived node script (no published CLI bin yet):

Terminal window
npx tsx node_modules/@mayflower-sys/evm-avm-client-demo/scripts/start-anvil.ts

When 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:

Terminal window
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:

Terminal window
jq . node_modules/@mayflower-sys/evm-avm-client-demo/fixtures/golden-manifest.json

Map 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.

Use the RPC URL, chain id, directory, and tenant id from Local above when filling OperatorConfig:

src/lib/config.ts
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:

src/lib/transport.ts
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
}

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.

scripts/create-market-group.ts
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:

Terminal window
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_GROUP_ID=my-group-1 npx tsx ./scripts/create-market-group.ts

Typical 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,
}

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 Local anvil, the generated group admin starts with zero ETH. Use Foundry cast to set its balance before Step 3:

Terminal window
ADDR=0xYourGroupAdminAddress
RPC=http://localhost:8545
WEI=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" --ether

Replace ADDR with groupAdmin from the Step 1 script output. WEI is 100 ETH (0x56bc75e2d63100000).

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.

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:

ParameterMeaning
slopeHow fast marginal price rises with supply on the main segment.
floorStarting guaranteed redemption price (reserve per AVM token).
rampScalarShoulder steepness multiplier (> 1; shoulder slope = scalar × slope).
rampEndSupplySupply coordinate at the end of the ramp shoulder (x₂); on-chain rampEndSupply.
rampWidthWidth 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.

scripts/create-market.ts
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 Address
const 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:

Terminal window
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.ts

Curve 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.

src/lib/read-market-state.ts
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),
}
}

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.

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:

MethodWhen to use
raiseFloorFromExcessLiquidityMarket holds surplus reserve above the curve area (e.g. after buys or donateLiquidity)
raiseFloorPreserveAreaSupply 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:

Terminal window
RESERVE=0xYourReserveToken
TO=0xYourGroupAdminAddress
RPC=http://localhost:8545
# 1,000,000 RESERVE @ 6 decimals
AMOUNT=1000000000000
# Anvil default account #0 — mock ERC-20 mint is unrestricted on the local fixture
KEY=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.

scripts/buy-and-donate-local.ts
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 Address
const 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.reserveTokenAddress
const 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:

Terminal window
RPC_URL=http://localhost:8545 CHAIN_ID=31337 MARKET_ADDRESS=0xabc... GROUP_ADMIN_PRIVATE_KEY=0xabc... npx tsx ./scripts/buy-and-donate-local.ts

Optional overrides: BUY_SHARES (default 100) and DONATE_AMOUNT (default 1000).

After 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.

After this guide you can:

  1. Open a market group under your tenant
  2. Create a linear Mayflower market with your chosen reserve token and curve params
  3. Display curve and reserve state in a TypeScript frontend
  4. Buy, donate, and raise the floor — create surplus reserve locally, then raise the floor as group admin

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.