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...
Environment variables
- VAULT_ID