Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zz-plant/whether/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Whether can deliver regime change alerts to your systems via webhooks, enabling automated workflows when market conditions shift.Alert Events
Whether generates alerts when significant regime changes occur:- Regime change: Transition to a new regime quadrant
- Tightness upshift/downshift: Monetary policy signal crosses threshold
- Risk appetite upshift/downshift: Market risk sentiment crosses threshold
Cooldown Logic
Alerts respect a 24-hour cooldown unless the regime flips again:// From lib/signalOps.ts
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const shouldCreateAlert = (
payload: SignalAlertPayload,
latestAlert?: RegimeAlertEvent
) => {
const hasRequiredTrigger = payload.reasons.some((reason) =>
[
"regime-change",
"tightness-upshift",
"tightness-downshift",
"risk-appetite-upshift",
"risk-appetite-downshift",
].includes(reason.code)
);
if (!hasRequiredTrigger) return false;
if (!latestAlert) return true;
const withinCooldown = Date.now() - Date.parse(latestAlert.createdAt) < ONE_DAY_MS;
const regimeFlippedAgain =
payload.currentAssessment.regime !== latestAlert.payload.currentAssessment.regime;
return !withinCooldown || regimeFlippedAgain;
};
Webhook Payload
When an alert is triggered, Whether delivers a webhook with this structure:{
id: string;
createdAt: string; // ISO 8601 timestamp
payload: {
previousRecordDate: string;
currentRecordDate: string;
previousAssessment: {
regime: string;
scores: { tightness: number; riskAppetite: number };
description: string;
};
currentAssessment: {
regime: string;
scores: { tightness: number; riskAppetite: number };
description: string;
};
reasons: Array<{
code: string;
message: string;
}>;
sourceUrls: string[];
timeMachineHref: string;
};
}
Example Payloads
Regime Change Alert
{
"id": "alert_1234567890",
"createdAt": "2025-03-03T10:00:00.000Z",
"payload": {
"previousRecordDate": "2025-02-21",
"currentRecordDate": "2025-02-28",
"previousAssessment": {
"regime": "CONSTRAINED_OPTIMISTIC",
"scores": { "tightness": 1.45, "riskAppetite": 0.82 },
"description": "Tight policy with strong risk appetite"
},
"currentAssessment": {
"regime": "CONSTRAINED_CAUTIOUS",
"scores": { "tightness": 1.52, "riskAppetite": -0.23 },
"description": "Tight policy with weak risk appetite"
},
"reasons": [
{
"code": "regime-change",
"message": "Regime shifted from CONSTRAINED_OPTIMISTIC to CONSTRAINED_CAUTIOUS"
},
{
"code": "risk-appetite-downshift",
"message": "Risk appetite crossed below threshold (0.82 → -0.23)"
}
],
"sourceUrls": [
"https://fiscaldata.treasury.gov/datasets/daily-treasury-par-yield-curve-rates/"
],
"timeMachineHref": "/report/time-machine?date=2025-02-28"
}
}
API Endpoints
Get Recent Alerts
GET /api/regime-alerts
{
"alerts": [
{
"id": "alert_1234567890",
"createdAt": "2025-03-03T10:00:00.000Z",
"payload": { /* ... */ }
}
]
}
Create Alert (Internal)
POST /api/regime-alerts
{
previousRecordDate: string;
currentRecordDate: string;
previousAssessment: RegimeAssessment;
currentAssessment: RegimeAssessment;
reasons: Array<{ code: string; message: string }>;
sourceUrls: string[];
timeMachineHref: string;
}
Manage Alert Preferences
GET /api/alert-preferences?clientId={clientId}
POST /api/alert-preferences
{
"clientId": "user_abc123",
"preferences": {
"slack": true,
"email": true,
"webhook": false
}
}
Deliver Alert
POST /api/alert-deliveries
{
"clientId": "user_abc123",
"alertId": "alert_1234567890",
"channels": ["slack", "email", "webhook"]
}
{
"deliveries": [
{
"id": "delivery_001",
"alertId": "alert_1234567890",
"channel": "slack",
"deliveredAt": "2025-03-03T10:01:00.000Z",
"status": "sent",
"summary": "CONSTRAINED_CAUTIOUS (2025-02-28) · regime-change, risk-appetite-downshift"
}
],
"preferences": {
"slack": true,
"email": true,
"webhook": false
}
}
Webhook Handlers
- Express.js
- Next.js API Route
- Python Flask
- Cloudflare Worker
const express = require("express");
const app = express();
app.post("/webhooks/whether", express.json(), (req, res) => {
const alert = req.body;
console.log("Whether regime alert:", {
id: alert.id,
regime: alert.payload.currentAssessment.regime,
reasons: alert.payload.reasons.map(r => r.code)
});
// Process alert (send notifications, update dashboards, etc.)
processRegimeChange(alert);
res.status(200).json({ received: true });
});
app.listen(3000);
// app/api/webhooks/whether/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const alert = await request.json();
// Validate alert structure
if (!alert.id || !alert.payload) {
return NextResponse.json(
{ error: "Invalid alert payload" },
{ status: 400 }
);
}
// Process regime change
await handleRegimeChange({
alertId: alert.id,
previousRegime: alert.payload.previousAssessment.regime,
currentRegime: alert.payload.currentAssessment.regime,
reasons: alert.payload.reasons,
});
return NextResponse.json({ received: true });
}
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhooks/whether", methods=["POST"])
def whether_webhook():
alert = request.get_json()
alert_id = alert.get("id")
payload = alert.get("payload", {})
current = payload.get("currentAssessment", {})
reasons = payload.get("reasons", [])
print(f"Whether alert {alert_id}: {current.get('regime')}")
for reason in reasons:
print(f" - {reason['code']}: {reason['message']}")
# Process the alert
process_regime_change(alert)
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)
export default {
async fetch(request: Request): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const alert = await request.json();
// Process the alert
await handleRegimeChange(alert);
return Response.json({ received: true });
},
};
async function handleRegimeChange(alert: any) {
const { payload } = alert;
const regime = payload.currentAssessment.regime;
const reasons = payload.reasons.map((r: any) => r.code);
console.log(`Regime change: ${regime} (${reasons.join(", ")})`);
// Send to Slack, Discord, etc.
}
Integration Examples
Slack Notifications
const sendSlackAlert = async (alert: RegimeAlertEvent) => {
const { payload } = alert;
const { currentAssessment, reasons } = payload;
const blocks = [
{
type: "header",
text: {
type: "plain_text",
text: `Regime Change: ${currentAssessment.regime}`,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*${payload.previousAssessment.regime}* → *${currentAssessment.regime}*`,
},
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Tightness:*\n${currentAssessment.scores.tightness.toFixed(2)}`,
},
{
type: "mrkdwn",
text: `*Risk Appetite:*\n${currentAssessment.scores.riskAppetite.toFixed(2)}`,
},
],
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Reasons:*\n${reasons.map((r) => `• ${r.message}`).join("\n")}`,
},
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "View Report" },
url: `https://whether.fyi${payload.timeMachineHref}`,
},
],
},
];
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ blocks }),
});
};
Email Notifications
import nodemailer from "nodemailer";
const sendEmailAlert = async (alert: RegimeAlertEvent) => {
const { payload } = alert;
const { currentAssessment, reasons } = payload;
const transport = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const html = `
<h2>Whether Regime Change Alert</h2>
<p><strong>${payload.previousAssessment.regime}</strong> → <strong>${currentAssessment.regime}</strong></p>
<h3>Details</h3>
<ul>
<li><strong>Tightness:</strong> ${currentAssessment.scores.tightness.toFixed(2)}</li>
<li><strong>Risk Appetite:</strong> ${currentAssessment.scores.riskAppetite.toFixed(2)}</li>
</ul>
<h3>Reasons</h3>
<ul>
${reasons.map((r) => `<li><strong>${r.code}:</strong> ${r.message}</li>`).join("")}
</ul>
<p><a href="https://whether.fyi${payload.timeMachineHref}">View full report</a></p>
`;
await transport.sendMail({
from: "alerts@yourdomain.com",
to: "team@yourdomain.com",
subject: `Whether: Regime changed to ${currentAssessment.regime}`,
html,
});
};
Discord Webhook
const sendDiscordAlert = async (alert: RegimeAlertEvent) => {
const { payload } = alert;
const { currentAssessment, reasons } = payload;
const embed = {
title: "Whether Regime Change",
description: `**${payload.previousAssessment.regime}** → **${currentAssessment.regime}**`,
color: 0x3b82f6, // blue
fields: [
{
name: "Tightness",
value: currentAssessment.scores.tightness.toFixed(2),
inline: true,
},
{
name: "Risk Appetite",
value: currentAssessment.scores.riskAppetite.toFixed(2),
inline: true,
},
{
name: "Reasons",
value: reasons.map((r) => `• **${r.code}:** ${r.message}`).join("\n"),
},
],
url: `https://whether.fyi${payload.timeMachineHref}`,
timestamp: alert.createdAt,
};
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
};
Security
Request Validation
Validate incoming webhooks to ensure they’re from Whether:import { z } from "zod";
const alertSchema = z.object({
id: z.string(),
createdAt: z.string().datetime(),
payload: z.object({
previousRecordDate: z.string(),
currentRecordDate: z.string(),
previousAssessment: z.object({
regime: z.string(),
scores: z.object({
tightness: z.number(),
riskAppetite: z.number(),
}),
}),
currentAssessment: z.object({
regime: z.string(),
scores: z.object({
tightness: z.number(),
riskAppetite: z.number(),
}),
}),
reasons: z.array(
z.object({
code: z.string(),
message: z.string(),
})
),
}),
});
export async function POST(request: Request) {
const body = await request.json();
// Validate structure
const result = alertSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: "Invalid payload", details: result.error },
{ status: 400 }
);
}
// Process the alert
await handleAlert(result.data);
return NextResponse.json({ received: true });
}
IP Allowlisting
Restrict webhook endpoints to Whether’s infrastructure:const ALLOWED_IPS = ["1.2.3.4", "5.6.7.8"]; // Whether IP ranges
export async function POST(request: Request) {
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip");
if (!ip || !ALLOWED_IPS.includes(ip)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Process webhook
}
Polling Alternative
If webhooks aren’t feasible, poll the agent API and check for new alerts:const checkForNewAlerts = async () => {
const response = await fetch("https://whether.fyi/api/regime-alerts");
const { alerts } = await response.json();
const latest = alerts[0];
const lastSeen = await getLastSeenAlertId();
if (latest && latest.id !== lastSeen) {
await handleRegimeChange(latest);
await saveLastSeenAlertId(latest.id);
}
};
// Poll every hour
setInterval(checkForNewAlerts, 60 * 60 * 1000);
Related
- Agent Interface - Programmatic access for AI agents
- API Reference - Complete endpoint documentation