Skip to content
Runner · ExamplesLadderrung 09

Multi-action atomic transaction

multi-action atomic tx

Mirrors runner/src/plugins/09-exampleMultiAction

What this teaches

  • decide() can return multiple Actions; the runtime bundles them into ONE atomic PTB on Sui
  • Useful when steps must succeed together: fund the predict manager and mint in the same tx, refresh NAV before crystallize, etc.
  • Mix mintBinary + mintRange + pmDeposit freely

New vs exampleOnExecuted

  • Returns 2-3 Actions per tick instead of 1
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { minutes } from "@automark/sdk/duration";

interface Signal {
  asset: string;
  direction: "up" | "down";
  conviction: number;        // 0..1
  hedge: boolean;
}

export default function createExampleMultiAction(): StrategyPlugin {
  const vaultId = process.env.VAULT_ID;
  if (!vaultId) throw new Error("exampleMultiAction: VAULT_ID not set");

  const signalUrl =
    process.env.SIGNAL_URL ?? "https://your-backend.example/signal";

  return {
    name: "exampleMultiAction",
    vaultId,
    triggers: [{ kind: "cron", everySeconds: 90 }],

    async decide(ctx) {
      
      if (ctx.vault.isFrozen) return [{ kind: "noop", reason: "frozen" }];

      const signal = (await (await fetch(signalUrl)).json()) as Signal;
      // Multi-action mints (directional + optional hedge) want a slightly
      // wider rolling window so hedges land in the same family of expiries.
      const market = await Market.find({
        asset: signal.asset,
        expiryAfterMs: ctx.now + minutes(5),
        expiringWithinMs: minutes(30),
        client: ctx.suiClient,
      });

      const p = await market.price();

      const ceiling = ctx.vault.maxSinglePosition;

      const directional = (ceiling * BigInt(Math.round(signal.conviction * 100))) / 200n;

      if (directional === 0n) return [{ kind: "noop", reason: "no size" }];

      // Build a multi-action tx that funds the PM (if needed) then mints
      // directional + an optional hedge range — all atomic.
      const isUp = signal.direction === "up";

      const directionalStrike = isUp
        ? market.strikeAbove(p.forwardRaw, { pctBps: 300 })
        : market.strikeBelow(p.forwardRaw, { pctBps: 300 });

      // deno-fmt-ignore
      // prettier-ignore
      // biome-ignore format: list reads clearer one-per-line
      const actions: Awaited<ReturnType<typeof this.decide>> = [
        { kind: "vault.pmDeposit", params: { amount: directional } },
        { kind: "vault.mintBinary", params: {
            marketId: market.id, strike: directionalStrike, isUp,
            quantity: directional,
          } },
      ];

      // Optional hedge — a range straddling the forward
      if (signal.hedge) {
        const hedgeSize = directional / 2n;
        actions.push({
          kind: "vault.mintRange",
          params: {
            marketId: market.id,
            lowerStrike: market.strikeBelow(p.forwardRaw, { pctBps: 200 }),
            higherStrike: market.strikeAbove(p.forwardRaw, { pctBps: 200 }),
            quantity: hedgeSize,
          },
        });
      }

      return actions;
    },
  };
}


// insights

// There is no limit to how many orders you can open at once, whether one, two, or ten. If your strategy finds it appropriate to enter 2 markets simultaneously, that is entirely possible.
// In the example strategy the signal can come with a 'hedge' flag, which means that besides a binary decision of being above or below value "X", it actually opens another position with a wider range of possibilities
// and thereby protects itself in the opposite case. (CHECK WHETHER THIS CONCLUSION OF MINE IS CORRECT)

Environment variables

  • VAULT_ID
  • SIGNAL_URL