Recipes
Each recipe builds on the skeleton from Your first app: verify the token on your server, then call facia(call, body).
#Coin raffle
Permissions: Take coins, Give coins, Post in groups, Send notifications.
// Enter: 20 coins per ticket. The ref makes a double-tap one charge.
app.post("/api/enter", async (req, res) => {
const c = client(req);
const ticket = `${raffle.id}:${c.sub}:${req.body.n}`;
const r = await facia("coins.charge", { sub: c.sub, amount: 20, reason: `Raffle ticket #${req.body.n}`, ref: `ticket:${ticket}` });
if (!r.ok) return res.json(r); // e.g. { error: "short", balance: 5 }
await db.tickets.insert({ biz: c.biz, sub: c.sub, alias: c.alias, raffle: raffle.id });
res.json(r);
});
// Draw (on a timer): pay the winner, tell the group, ping the winner.
async function draw(raffle) {
const tickets = await db.tickets.find({ raffle: raffle.id });
const win = tickets[crypto.randomInt(tickets.length)];
const pot = tickets.length * 20;
await facia("coins.pay", { sub: win.sub, amount: pot, reason: "Raffle win", ref: `win:${raffle.id}` });
await facia("group.post", { group: raffle.groupId, text: `๐ ${win.alias} won the raffle: ${pot} coins!` });
await facia("push.send", { sub: win.sub, title: "You won the raffle!", text: `${pot} coins are yours.` });
}#Scored game with a leaderboard
Permissions: Post scores, Give badges.
app.post("/api/score", async (req, res) => {
const c = client(req);
const score = validateRun(req.body.run); // re-check the run on the server; never trust a raw score from the browser
const r = await facia("board.score", { sub: c.sub, board: "runner", value: score });
if (r.ok && r.rank === 1) await facia("badge.give", { sub: c.sub, badge: "runner-no1", label: "Runner #1", icon: "๐" });
res.json(r);
});The board starts fresh on the business's leaderboard period (day, week or month). best is the client's best this period.
#Daily check-in mission
Permissions: Count mission events.
The business creates a Mission block with Done when โ An app reports it, event checkin, target 5, reward 100 coins. Your app reports each check-in:
app.post("/api/checkin", async (req, res) => {
const c = client(req);
const day = new Date().toISOString().slice(0, 10);
if (await db.checkins.exists({ biz: c.biz, sub: c.sub, day })) return res.json({ ok: true, already: true });
await db.checkins.insert({ biz: c.biz, sub: c.sub, day });
res.json(await facia("mission.event", { sub: c.sub, event: "checkin" }));
});Facia counts it, and pays the reward when the client reaches the target. Your app never touches coins.
#Members-only shop (coins)
Permissions: See a balance, Take coins. Audience: Verified members.
app.post("/api/buy", async (req, res) => {
const c = client(req);
const item = catalog[req.body.item];
const order = await db.orders.create({ biz: c.biz, sub: c.sub, item: item.id, status: "pending" });
const r = await facia("coins.charge", { sub: c.sub, amount: item.price, reason: item.name, ref: `order:${order.id}` });
await db.orders.update(order.id, { status: r.ok ? "paid" : "failed" });
res.json(r);
});Coins are not money. For real-money products, send the client to your own checkout outside Facia. Never take card details inside the frame.
#Staff tool
Set Who sees it to Your team only. The token has role: "team" and alias = the staff member's display name. Use it for an internal dashboard, an order lookup or a stock board. You get the business's look and sign-in for free.
if (c.role !== "team") return res.status(403).end();#Handling rate limits and retries
async function faciaRetry(call, body, tries = 3) {
for (let i = 0; i < tries; i++) {
const res = await fetch(`${process.env.FACIA_API}/${call}`, { method: "POST",
headers: { Authorization: `Bearer ${process.env.FACIA_APP_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify(body) });
const data = await res.json();
if (res.status === 429) { await new Promise((r) => setTimeout(r, data.retryAfterMs ?? 1000)); continue; }
if (res.status >= 500) { await new Promise((r) => setTimeout(r, 1000 * 2 ** i)); continue; }
return data;
}
throw new Error(`Facia ${call} failed`);
}Coin calls are safe to retry with the same ref.