§ the implementation
the implementation
the implementation of fable is a small typescript service, roughly four hundred lines, deployed as three supabase edge functions against bsc mainnet. the first function is the loop: it wakes on a schedule, reads market state from birdeye, asks fable 5 for a belief, sizes the belief with the kelly fraction, signs the swap, and commits the forecast. the second is the scorer: it wakes after each horizon closes and marks every open forecast against what the market did. the third is the proxy: it serves birdeye data to the site without ever exposing the key. all secrets live server side. nothing on the client can sign, fetch with credentials, or write.
the loop
// the agent loop. runs on a cron schedule. one cycle, one decision.
import { Wallet, JsonRpcProvider } from "npm:ethers";
const ANTHROPIC_KEY = Deno.env.get("ANTHROPIC_API_KEY")!;
const BIRDEYE_KEY = Deno.env.get("BIRDEYE_API_KEY")!;
const RPC = new JsonRpcProvider(Deno.env.get("BNB_RPC_URL")!);
const AGENT = new Wallet(Deno.env.get("AGENT_SECRET_KEY")!, RPC);
const TOKEN = "<BNB_TOKEN_ADDRESS>";
const HALF = 0.5; // fractional kelly
const MAX_F = 0.05; // hard ceiling per position
const HORIZON_MIN = 240; // forecast horizon in minutes
Deno.serve(async () => {
// 1. read market state from birdeye
const market = await birdeye(`/defi/token_overview?address=${TOKEN}`);
const candles = await birdeye(`/defi/ohlcv?address=${TOKEN}&type=15m&limit=96`);
const book = await birdeye(`/v1/wallet/token_list?wallet=${AGENT.address}`);
// 2. ask fable 5 for a belief
const belief = await getBelief(market, candles, book);
// 3. size with half kelly. p_up below one half is a sell of equal edge
const f = kellyFraction(belief.p_up);
const bankroll = await bnbBalance(AGENT.address);
const size = Math.floor(bankroll * f);
// 4. the forecast is committed before anything is signed
const forecast = await insertForecast({
token: TOKEN, p: belief.p_up, horizon_min: HORIZON_MIN, f, size,
});
// 5. the correct size for no edge is zero. most cycles end here
if (size === 0) return new Response("flat");
// 6. build, sign, land the swap through pancakeswap
const side = belief.p_up > 0.5 ? "buy" : "sell";
const sig = await pancakeSwap(AGENT, TOKEN, size, side);
await attachSignature(forecast.id, sig);
return new Response(sig);
});
function kellyFraction(p: number): number {
const edge = Math.abs(2 * p - 1); // even money: f* = p minus q
const f = edge * HALF; // half kelly
return Math.min(f, MAX_F); // hard ceiling
}the belief
// the model is scored by log loss against the realized outcome.
// calibration is the only instruction that matters.
async function getBelief(market: unknown, candles: unknown, book: unknown) {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": ANTHROPIC_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-fable-5",
max_tokens: 300,
system:
"you are the belief engine of a kelly-sized trading agent. " +
"read the market state and return json only: " +
'{"p_up": <probability the price is higher at horizon>, "note": <one sentence>}. ' +
"your p_up will be scored by log loss against the realized outcome. " +
"if you have no edge, say 0.5.",
messages: [{ role: "user", content: JSON.stringify({ market, candles, book }) }],
}),
});
const data = await res.json();
return JSON.parse(data.content[0].text);
}the data layout
-- insert only. public read. service role write. no update, no delete.
create table forecasts (
id bigint generated always as identity primary key,
made_at timestamptz not null default now(),
mint text not null,
p numeric not null check (p > 0 and p < 1),
horizon_min int not null,
f numeric not null,
size_lamports bigint not null,
tx_sig text,
price_at_forecast numeric,
price_at_horizon numeric,
outcome int, -- 1 if up, 0 if down, null until scored
log_score numeric, -- ln(p) if outcome is 1, ln(1 minus p) otherwise
brier numeric,
scored_at timestamptz
);
alter table forecasts enable row level security;
create policy public_read on forecasts for select using (true);
-- no insert, update, or delete policies. writes happen through the service role only.
the scorer
// runs on a cron offset. closes every forecast whose horizon has elapsed.
Deno.serve(async () => {
const open = await openForecasts();
for (const fc of open) {
const p1 = await priceAtHorizon(fc.mint, fc.made_at, fc.horizon_min);
const outcome = p1 > fc.price_at_forecast ? 1 : 0;
const logScore = Math.log(outcome === 1 ? fc.p : 1 - fc.p);
const brier = (fc.p - outcome) ** 2;
await score(fc.id, { price_at_horizon: p1, outcome, log_score: logScore, brier });
}
return new Response(`scored ${open.length}`);
});the seal
four secrets exist, and none of them ever touch the client: the anthropic key, the birdeye key, the agent's secret key, and the service role key. the site is a reader. it pulls market data through the proxy function and pulls the record from the public forecast table, and that is the entire surface. the forecast table has no update path and no delete path at the policy level, which means the record the site renders is the record that exists. the wallet address is printed at the bottom of the page. the chain is the audit. the table is the record. the score is the verdict. nothing else is in the path.