Skip to content
Runner · ExamplesLadderrung 07

Consensus from multiple signal sources

multi-source consensus voting

Mirrors runner/src/plugins/07-exampleConsensus

What this teaches

  • Plugin orchestrates multiple inputs (e.g. your model + a sentiment feed + an on-chain oracle), runs a vote, acts only on agreement
  • Promise.all keeps the tick fast — fetches run in parallel
  • Failure of one source is non-fatal — partial consensus still works

New vs exampleCombo

  • Multiple sources, voting logic, fault-tolerant fetch
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { hours, minutes } from "@automark/sdk/duration";

interface SourceVote {
  action: "buy" | "sell" | "hold";
}

const MIN_AGREEMENT = 2;   // need at least 2 sources voting the same way

export default function createExampleConsensus(): StrategyPlugin {

  const vaultId = process.env.VAULT_ID;

  if (!vaultId) throw new Error("exampleConsensus: VAULT_ID not set");

  const sources = (
    process.env.SOURCES ??
    "https://signal-1.example/btc,https://signal-2.example/btc,https://signal-3.example/btc"
  )
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);

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

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

      // Fetch all sources in parallel; tolerate individual failures
      const votes = await Promise.allSettled(
        sources.map(async (url) => {
          const res = await fetch(url);
          return (await res.json()) as SourceVote;
        }),
      );

      const tally = { buy: 0, sell: 0, hold: 0 };

      for (const v of votes) {
        if (v.status === "fulfilled") tally[v.value.action]++;
      }

      ctx.logger.info("consensus tally", tally);

      const winner =
        tally.buy >= MIN_AGREEMENT
          ? "buy"
          : tally.sell >= MIN_AGREEMENT
            ? "sell"
            : null;

      if (!winner) return [{ kind: "noop", reason: "no consensus" }];

      // Consensus signals are slower to converge — give them more room
      // (15min to 2h). Pattern A: rolling window relative to now.
      const btc = await Market.find({
        asset: "BTC",
        expiryAfterMs: ctx.now + minutes(15),
        expiringWithinMs: hours(2),
        client: ctx.suiClient,
      });


      const p = await btc.price();

      const quantity = ctx.vault.maxSinglePosition / 3n;

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

      const isUp = winner === "buy";

      return [
        {
          kind: "vault.mintBinary",
          params: {
            marketId: btc.id,
            strike: isUp
              ? btc.strikeAbove(p.forwardRaw, { pctBps: 300 })
              : btc.strikeBelow(p.forwardRaw, { pctBps: 300 }),
            isUp,
            quantity,
          },
        },
      ];
    },
  };
}


// insights we can take from this example:

// You can build quorum mechanisms, where the final decision is determined by the consensus of people, services, or institutions.

Environment variables

  • VAULT_ID
  • SOURCES (CSV of URLs, default mocked)