npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mayflower-sys/avm-sim

v202608.1222.849

Published

Standalone AVM simulator: an immutable, log-driven state machine over avm-calc for charting, notebooks, and projections.

Readme

@mayflower-sys/avm-sim

A standalone AVM simulator: an immutable record that wraps an avm-calc calculator together with an ordered log of actions. It is a low-level, general-purpose engine for charting apps, desktop simulations, in-app projections, and Observable notebooks.

Everything is a pure synchronous function over plain number boundaries. Operations never mutate and never throw: a partial operation returns Either<Simulation, SimulationError>, and a quote that may not exist returns Option<number>.

creation parameters      -> blank Simulation           (simulation.ts)
request + current curve  -> recorded LogPayload        (operations.ts)
Simulation + LogPayload  -> next Simulation            (apply.ts)
logs                     -> folded totals              (derived.ts)
current curve            -> geometry, quotes, capacity (derived/quotes/capacity)

The record

interface Simulation {
  readonly calculator: AvmCalculator<number> // compiled from the whole history
  readonly logs: ReadonlyArray<Log> // the source of truth
}

The log is an event-sourced history. Payloads are facts, not requests: both sides of every trade are stored at the amounts the curve quoted when the action was accepted, so a log can be folded and replayed without re-quoting.

| Payload | Fields | Curve transition | | ----------------- | ------------------------ | ------------------------------- | | Issuance | reserveIn, sharesOut | supply moves up the curve | | Redemption | sharesIn, reserveOut | supply moves down; may contract | | RedeemAtFloor | sharesIn, reserveOut | whole schedule translates left | | ExerciseOptions | reserveIn, sharesOut | whole schedule translates right | | Borrow | reserveOut | none — folded balances only | | Repay | reserveIn | none — folded balances only | | DonateReserve | reserveIn | none — creates reserve surplus |

There is no per-account position tracking: all supply is treated as pledged collateral, so borrow capacity is the floor value of the whole supply minus the outstanding draw.

Quick start

import { Either, Option, pipe } from "effect"
import * as Sim from "@mayflower-sys/avm-sim"

const outcome = pipe(
  Sim.makeLinearSimulation({ slope: 2, initialFloor: 5 }), // blank linear AVM
  Either.flatMap(Sim.issueShares({ reserveIn: 36 })), // buy shares for 36 reserves
  Either.flatMap(Sim.redeemShares({ sharesIn: 1 })), // sell 1 share along the curve
  Either.flatMap(Sim.borrow({ reserveOut: 0.5 })), // borrow 0.5 against floor collateral
)

const sim = Either.getOrThrow(outcome)

sim.logs // three facts, indexes 0..2
Sim.totalSupply(sim) // ≈ 3, folded from the log
Sim.netReserves(sim) // 36 − 12 − 0.5
Sim.spotPrice(sim) // 11
Sim.floorPrice(sim) // 5

// Floor raise is not a logged operation yet — probe how far a legal
// area-preserving raise could go (needs supply strictly above the ramp end).
const maxRise = Sim.maxAreaPreservingFloorRise(sim) // Option<number>
Option.getOrThrow(maxRise) // positive; capped below spot − floor

Every operation supports both call styles: Sim.issueShares(sim, request) and pipe(sim, Sim.issueShares(request)). A refused operation is a Left with a tagged reason (InsufficientSupply, ExceedsFloorRegion, ExceedsBorrowCapacity, ExceedsBorrowedBalance, InvalidRequest, CalculatorRejection) — match on it with SimulationError.$match.

Creating a simulation

Two factories build a blank record — empty log, zero supply, zero-width ramp at zero, spot price on the floor:

Sim.makeLinearSimulation({
  slope: 0.001, // positive main slope
  initialFloor: 0.1, // launch floor price f₀
  tokenNormalizationFactor: 1, // optional affine x-scale, default 1
  rampScalar: 2, // optional ramp steepness multiple, default 2
})

Sim.makeHingedExponentialSimulation({
  m: 1, // positive slope of the linear component
  h: 1, // nonnegative hinge: supply where the exponential premium begins
  q: 2, // nonnegative curvature coefficient
  k: 0.5, // signed exponential intensity
  initialFloor: 1, // launch floor price f₀
  tokenNormalizationFactor: 1, // optional affine x-scale, default 1
  rampScalar: 2, // optional ramp steepness multiple, default 2
})

Creation parameters are validated by Effect Schema; failures are returned as values such as Left(InvalidRequest), never as a thrown exception. rampScalar only matters once the floor rises (a future operation), but it is persisted state on the wrapped calculator, so it is settable at creation; 2 mirrors the canonical "ramp twice as steep as main" shape.

Derived data

Log folds (each is a sum of one Flow field over the log — see flowOf):

| Function | Meaning | | ----------------------------------- | ---------------------------------------- | | totalSupply | shares outstanding | | totalBorrowed | outstanding borrowed reserves | | netReserves | liquid reserve balance | | grossReserves | liquid balance plus outstanding borrowed | | totalReserveIn, totalReserveOut | cumulative reserve volumes |

Curve reads: requiredReserves (area up to supply), reserveSurplus (gross − required; donations are the only source), borrowCapacity, floorPrice, spotPrice, rampStart/rampEnd/rampWidth (world-space), and the persisted affineParameters / segmentationParameters — the x-translation moves with option exercise and floor redemption, the y-translation with sell contraction.

Floor-rise capacities, each a legal witness within the accuracy contract:

  • maxAreaPreservingFloorRise — largest rise an area-preserving raise can achieve now; None unless supply is strictly above the ramp end.
  • maxFloorRiseFromSurplus — largest rise the donated surplus can fund with the schedule above the new floor unchanged, capped at the spot price.
  • requiredSupplyForFloorRise(rise) — the minimum supply at which a raise of that size becomes legal (the solved ramp end of the raise).
  • requiredReservesAtFloor(target) — the solvency requirement if the floor stood at target with the curve above it unchanged.

Every capacity is a generic iterative search — bisection or a doubling probe — over the calculator's own operations. This is a deliberate design rule for the whole package: prefer simple, generic solvers over closed-form special cases (closed forms appear only in tests, as independent oracles). Every capacity takes an optional work budget as a trailing parameter (SearchBudget { iterations? } for the bisections, DoublingBudget { maxDoublings? } for the supply probe) and threads it straight through to its solver; the defaults resolve far past the 0.1% accuracy contract. Budgets stay plain parameters at this layer so a wrapping consumer can add whatever ergonomic layer it wants. The solvers themselves — largestSatisfying, leastReaching, firstAccepted — are exported for consumers building their own derived searches.

Trade quotes that never touch the log: quoteReservesInForSharesOut, quoteSharesOutForReservesIn, quoteReservesOutForSharesIn, quoteSharesInForReservesOut, each returning Option<number>. Running an operation on a copy is an equally valid way to preview an outcome — the record is immutable either way.

Logs and replay

Each log entry is { index, payload }. Walk the history with LogPayload.$match for a narrative, and flowOf for signed balance deltas (reserves, supply, borrowed) — the same fields the derived totals fold:

let supply = 0
let reserves = 0
let borrowed = 0

for (const { index, payload } of sim.logs) {
  const flow = Sim.flowOf(payload)
  supply += flow.supply
  reserves += flow.reserves
  borrowed += flow.borrowed

  const line = Sim.LogPayload.$match(payload, {
    Issuance: ({ reserveIn, sharesOut }) =>
      `buy ${sharesOut} shares for ${reserveIn} reserves`,
    Redemption: ({ sharesIn, reserveOut }) =>
      `sell ${sharesIn} shares for ${reserveOut} reserves`,
    RedeemAtFloor: ({ sharesIn, reserveOut }) =>
      `floor-redeem ${sharesIn} → ${reserveOut}`,
    ExerciseOptions: ({ reserveIn, sharesOut }) =>
      `exercise ${sharesOut} options for ${reserveIn}`,
    Borrow: ({ reserveOut }) => `borrow ${reserveOut}`,
    Repay: ({ reserveIn }) => `repay ${reserveIn}`,
    DonateReserve: ({ reserveIn }) => `donate ${reserveIn}`,
  })

  console.log(`#${index}`, line, { supply, reserves, borrowed })
}

Price is not stored on the payload — it comes from the curve after each step. Fold with applyPayload (or replay) and read spotPrice / floorPrice on the rebuilt simulation:

const blank = Either.getOrThrow(
  Sim.makeLinearSimulation({ slope: 2, initialFloor: 5 }),
)

let cursor = blank

for (const { index, payload } of sim.logs) {
  cursor = Either.getOrThrow(Sim.applyPayload(cursor, payload))

  const line = Sim.LogPayload.$match(payload, {
    Issuance: ({ reserveIn, sharesOut }) =>
      `buy ${sharesOut} shares for ${reserveIn} reserves`,
    Redemption: ({ sharesIn, reserveOut }) =>
      `sell ${sharesIn} shares for ${reserveOut} reserves`,
    RedeemAtFloor: ({ sharesIn, reserveOut }) =>
      `floor-redeem ${sharesIn} → ${reserveOut}`,
    ExerciseOptions: ({ reserveIn, sharesOut }) =>
      `exercise ${sharesOut} options for ${reserveIn}`,
    Borrow: ({ reserveOut }) => `borrow ${reserveOut}`,
    Repay: ({ reserveIn }) => `repay ${reserveIn}`,
    DonateReserve: ({ reserveIn }) => `donate ${reserveIn}`,
  })

  console.log(`#${index}`, line, {
    spot: Sim.spotPrice(cursor),
    floor: Sim.floorPrice(cursor),
    supply: Sim.totalSupply(cursor),
    reserves: Sim.netReserves(cursor),
    borrowed: Sim.totalBorrowed(cursor),
  })
}

For the fill price of a single trade (not the post-trade spot), divide the recorded legs on the payload — borrow, repay, and donate have no curve effect, so spot is unchanged across those steps:

const fill = Sim.LogPayload.$match(payload, {
  Issuance: ({ reserveIn, sharesOut }) => reserveIn / sharesOut,
  Redemption: ({ sharesIn, reserveOut }) => reserveOut / sharesIn,
  RedeemAtFloor: ({ sharesIn, reserveOut }) => reserveOut / sharesIn,
  ExerciseOptions: ({ reserveIn, sharesOut }) => reserveIn / sharesOut,
  Borrow: () => undefined,
  Repay: () => undefined,
  DonateReserve: () => undefined,
})

applyPayload is the single enforcement path for every state change; operations quote a request into a payload and come through it. replay folds recorded payloads onto a blank record, so persisting creation parameters plus logs.map((log) => log.payload) deterministically reconstructs the simulation:

const blank = Either.getOrThrow(
  Sim.makeLinearSimulation({ slope: 2, initialFloor: 5 }),
)
const rebuilt = Either.getOrThrow(
  Sim.replay(
    blank,
    sim.logs.map((log) => log.payload),
  ),
)
// rebuilt mirrors sim: same totals, same curve geometry

Payload amounts are trusted facts on replay; only structural validity (enough supply, floor-region width, borrow capacity) is re-checked.

Accuracy

The wrapped calculator targets 0.1% relative accuracy and on-chain settlement is always authoritative. Exact-side amounts (reserveIn on issuance, sharesIn on redemption, floor-priced legs, borrows, donations) are recorded verbatim; solver-quoted legs inherit the calculator's contract, and the capacity searches return accepted witnesses, so they are legal and at most a hair conservative. Logs are plain float sums — the simulator is built for about a hundred operations, not for settlement.

Future operations

The log and reducer are the extension seam. Planned next: an explicit floor raise (the ramp already shifts right through raiseFloorPreserveArea on the wrapped calculator) and leverage loops composed from the existing borrow and issue primitives.