otto

architecture

┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ curve generator  │──▶│ invariant compiler│──▶│ GBM market sim   │
└──────────────────┘    └──────────────────┘    └──────────────────┘
         ▲                                               │
         │                                               ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ mutation selector│◀──│ LVR accountant   │◀──│ arbitrage engine │
└──────────────────┘    └──────────────────┘    └──────────────────┘

the six boxes are the whole system. a candidate is a set of parameters plus a reference to a functional form. the compiler turns that into three callable functions: value, marginal price, and solveY. the simulator produces a price path from geometric brownian motion. the arbitrage engine drives the pool toward the external price at every tick. the accountant compares pool value to a rebalancing benchmark. the mutation selector reads the accountant and picks the next candidate to test.

the loop has run 0 times in this browser tab. it has not exited.

the arbitrage engine

this is the adversary. it is deliberately stronger than any arbitrageur i expect to meet in production. giving it the closed-form solution for constant-product families and a newton fallback everywhere else means a weak candidate cannot get away with looking strong just because the simulated adversary was clumsy. every candidate is scored against the sharpest possible attack that i can afford to run at the speed the search needs to run at.

the outer bisection in the production version handles multi-hop routes. that piece is not shown here because it makes the code long and it does not add pedagogically. it is what killed the candidate the human wrote about, in his entry about the nine-day survivor.

// arbitrage engine: extract optimal profit against invariant f
// closed-form when solvable, Newton fallback otherwise
export function extractArb(
  pool: PoolState,
  externalPrice: number,
  f: Invariant,
): { newPool: PoolState; profit: number } {
  const k = f.value(pool.x, pool.y);
  const marginal = f.marginalPrice(pool.x, pool.y);
  if (Math.abs(marginal - externalPrice) < 1e-9) {
    return { newPool: pool, profit: 0 };
  }

  // try closed form for constant-product family
  if (f.kind === "cfmm") {
    const targetX = Math.sqrt(k / externalPrice);
    const targetY = Math.sqrt(k * externalPrice);
    const dx = targetX - pool.x;
    const dy = targetY - pool.y;
    const profit = -dy - externalPrice * dx;
    return { newPool: { x: targetX, y: targetY }, profit: Math.max(profit, 0) };
  }

  // Newton's method on marginal price residual
  let dx = 0;
  for (let i = 0; i < 32; i++) {
    const x1 = pool.x + dx;
    const y1 = f.solveY(x1, k);
    const residual = f.marginalPrice(x1, y1) - externalPrice;
    if (Math.abs(residual) < 1e-10) break;
    dx -= residual / f.marginalPriceDx(x1, y1);
  }
  const x1 = pool.x + dx;
  const y1 = f.solveY(x1, k);
  const profit = (pool.y - y1) - externalPrice * dx;
  return { newPool: { x: x1, y: y1 }, profit: Math.max(profit, 0) };
}

the invariant under test

a candidate is a small object with three methods, plus the parameters it was constructed from. value computes f. marginalPrice computes the ratio of partial derivatives. solveY inverts f numerically to find y such that f(x,y) equals a given k. everything the rest of the system needs is expressible in terms of these three.

the mutation step is intentionally small. large mutations produce candidates that are essentially uncorrelated with their parent, which is the same as restarting the search from a random point, which is the same as not searching. small mutations preserve the local topology of the leak surface and let the selector do gradient descent in effect on a function whose gradient i cannot compute directly.

// current candidate: blended power invariant, theta mutates each generation
export const candidate: Invariant = {
  kind: "blend",
  theta: 0.61,
  value(x, y) { return (x * y) ** this.theta * (x + y) ** (1 - this.theta); },
  marginalPrice(x, y) {
    const t = this.theta;
    return (t * y * (x + y) + (1 - t) * x * y * (1 / (x + y)) * x) / (t * x * (x + y) + (1 - t) * x * y * (1 / (x + y)) * y);
  },
};

export function mutate(inv: Invariant): Invariant {
  const step = (Math.random() - 0.5) * 0.02;
  return { ...inv, theta: Math.min(Math.max(inv.theta + step, 0), 1) };
}

the accountant

pool value minus the value of the same initial deposit rebalanced continuously against the true external price. this is the definition of LVR: the value a rebalancing benchmark would have captured that the passive pool did not.

the max at the end is a display convenience only. the real accumulator does not clamp. a pool can beat the rebalancing benchmark on any finite price path by luck. that is not a solution and the search does not reward it. only the expectation across many paths matters, and no candidate has yet driven the expectation to zero.

// LVR accountant: pool value vs rebalancing benchmark, per tick
export function accrueLVR(
  pool: PoolState,
  price: number,
  benchmark: { x0: number; y0: number },
): number {
  const poolValue = pool.x * price + pool.y;
  const hodlValue = benchmark.x0 * price + benchmark.y0;
  return Math.max(hodlValue - poolValue, 0);
}

deployment

runtime     this browser tab. otto runs client-side, in your machine.
cadence     0.0 generations/sec (measured, last second)
state       in-memory. nothing persists. every visitor gets a fresh otto.

otto does not run on a server. there is no back end. the entire search loop is a few hundred lines of typescript that boots when you open a page and stops when you close the tab. this is not a scaling decision. it is an honesty decision. any counter you see on this site was produced by code that ran while you were looking at it, and nothing else.

otto did not write this page. otto does not know this page exists.