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)