Skip to content
Runner · ExamplesLadderrung 10

Failure-aware: learn from on-chain aborts (TTL-based ban)

ban strikes after aborts

Mirrors runner/src/plugins/10-exampleFailureAware

What this teaches

  • onExecuted reacts to specific Move abort codes, persists a "ban" for the offending strike, and decide() respects the ban next tick
  • ctx.state.setWithTTL — ban entry auto-expires, no `bannedUntilMs` payload field, no cleanup pass. Just `has(banKey)` to check.
  • Move abort strings leak the code (e.g. "EFairPriceAlreadySettled")

New vs exampleMultiAction

  • Closed loop: failures alter future decisions
  • Storage uses setWithTTL — entries vanish on their own
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { minutes } from "@automark/sdk/duration";

const BAN_DURATION_MS = minutes(10);

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

  const banKey = (marketId: string, strike: bigint) => `ban:${marketId}:${strike}`;

  return {
    name: "exampleFailureAware",
    vaultId,
    triggers: [{ kind: "cron", everySeconds: 75 }],

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

      const btc = await Market.find({
        asset: "BTC",
        expiryAfterMs: ctx.now + minutes(5),
        client: ctx.suiClient,
      });
      const p = await btc.price();
      const strike = btc.strikeAbove(p.forwardRaw, { pctBps: 200 });

      if (await ctx.state.has(banKey(btc.id, strike))) {
        return [{ kind: "noop", reason: "strike banned (TTL still active)" }];
      }

      const quantity = ctx.vault.maxSinglePosition / 4n;
      if (quantity === 0n) return [{ kind: "noop", reason: "no headroom" }];

      return [
        {
          kind: "vault.mintBinary",
          params: { marketId: btc.id, strike, isUp: true, quantity },
        },
      ];
    },

    async onExecuted(ctx, result) {
      if (result.outcome !== "failed") return;
      const isDegenerate =
        result.error?.includes("EFairPriceAlreadySettled") ||
        result.error?.includes("EInvalidStrike");
      if (!isDegenerate) return;

      for (const action of result.actions) {
        if (action.kind !== "vault.mintBinary") continue;
        // setWithTTL: entry auto-expires after BAN_DURATION_MS, no cleanup
        // needed. The value is informational — `has()` checks presence.
        await ctx.state.setWithTTL(
          banKey(action.params.marketId, action.params.strike),
          { reason: result.error },
          BAN_DURATION_MS,
        );
        ctx.logger.warn("banned strike", {
          marketId: action.params.marketId,
          strike: action.params.strike.toString(),
          ttlMs: BAN_DURATION_MS,
        });
      }
    },
  };
}


// insights

// not sure what to say...
Go deeperLifecycle hooksonExecuted and onError react to the result of each tick without breaking the next one.

Environment variables

  • VAULT_ID