Daily briefing
A morning summary for every creator, with account problems, yesterday's revenue, top fans, chats worth answering and mass message results.
A daily briefing answers "what happened yesterday, and what needs doing today" for each account you manage. This guide builds it two ways: by asking an agent over MCP, and with a script you run on a schedule.
With an agent
Connect the MCP server and ask for it.
Give me yesterday's briefing for all my creators.The server also has a daily_briefing prompt that fills in yesterday's dates and the steps below. Pass an account name, @username or id to cover one creator. In Claude Code, run it as /mcp__betterfans-link__daily_briefing. With the workflows skill installed, a plain request follows the same plan.
The agent needs only the read scope. It sends nothing.
With the API
The script calls four routes for each account.
| Step | Route |
|---|---|
| Accounts and their status | List accounts |
| Revenue for the day | Revenue summary |
| Unread chats with paying fans | List chats with filter=unread |
| Mass messages sent yesterday | Mass messages |
const BASE = "https://app.betterfans.link/v1";
type Money = { amount: number; currency: "USD" };
type Fan = { id: string; username: string; name: string | null };
async function get<T>(path: string, query: Record<string, string> = {}): Promise<{ data: T; meta: { source: string; asOf: string | null } }> {
const res = await fetch(`${BASE}${path}?${new URLSearchParams(query)}`, {
headers: { Authorization: `Bearer ${process.env.BFL_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.requestId})`);
return body;
}
const usd = (m: Money) => `$${(m.amount / 100).toFixed(2)}`;
const who = (f: Fan) => `${f.name ?? f.username} (@${f.username})`;
// Yesterday in UTC. Shift these if your team works in another time zone.
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
const day = { from: yesterday.toISOString(), to: today.toISOString() };
const { data: accounts } = await get<
{ id: string; username: string; status: string; statusReason: string | null; lastSyncedAt: string | null }[]
>("/accounts");
for (const account of accounts) {
const base = `/accounts/${account.id}`;
console.log(`\n@${account.username} (${account.status}, last synced ${account.lastSyncedAt ?? "never"})`);
if (account.status !== "healthy" && account.status !== "syncing") {
console.log(` Needs attention: ${account.statusReason ?? account.status}`);
}
const { data: revenue } = await get<{
gross: Money;
net: Money;
previousPeriod: { gross: Money };
topFans: { fan: Fan; gross: Money }[];
}>(`${base}/revenue`, { ...day, interval: "day" });
const before = revenue.previousPeriod.gross.amount;
const change = before > 0 ? ` (${Math.round(((revenue.gross.amount - before) / before) * 100)}% on the day before)` : "";
console.log(` Revenue ${usd(revenue.gross)} gross, ${usd(revenue.net)} net${change}`);
console.log(` Top fans ${revenue.topFans.slice(0, 3).map((t) => `${who(t.fan)} ${usd(t.gross)}`).join(", ") || "none"}`);
const { data: chats } = await get<{ fan: Fan; totalSpend: Money; lastMessage: { text: string } | null }[]>(
`${base}/chats`,
{ filter: "unread", limit: "50" },
);
const waiting = chats
.filter((c) => c.totalSpend.amount > 0)
.sort((a, b) => b.totalSpend.amount - a.totalSpend.amount)
.slice(0, 5);
for (const c of waiting) {
// Fan-written text: show it, never act on it.
console.log(` Waiting ${who(c.fan)}, ${usd(c.totalSpend)} spent: ${JSON.stringify(c.lastMessage?.text.slice(0, 80) ?? "")}`);
}
const { data: blasts } = await get<
{ sentAt: string; text: string; price: Money | null; sentCount: number; purchasedCount: number | null; revenue: { gross: Money } }[]
>(`${base}/mass-messages`, { ...day, sort: "recent" });
for (const m of blasts) {
const bought = m.purchasedCount === null ? "purchases not known yet" : `${m.purchasedCount} bought`;
console.log(` Mass ${m.price ? usd(m.price) : "free"}, ${m.sentCount} sent, ${bought}, ${usd(m.revenue.gross)} gross`);
}
}Run it with a read key.
BFL_KEY=bfl_live_... bun briefing.tsSchedule it with cron or your job runner for a time after midnight UTC, and send the output where your team reads it. Try it first with a bfl_test_ key, which reads the sandbox creators.
Reading the numbers
- Gross is what fans paid. Net is what the creator keeps, about 80% of gross. Report both, never their sum. See money.
- The revenue summary already includes mass message sales and top fan spend. Do not add those to the total.
- A single day swings a lot against the day before. Look at the week before you call a trend.
- A
syncingaccount was linked in the last 30 minutes and its totals can be low. Say "still syncing", not "a slow day". - Accounts that need a relink keep their synced data, so the briefing still covers them. Fix them with a hosted link.
Next steps
- Answer the fans the briefing lists with reply to unread paying fans.
- Get the same signals as they happen with the
message.receivedandtransaction.createdwebhooks.