Once a day, pull what happened from the API, write it down, and check it against your bank: payments in, refunds out, and payouts to your bank with their trace numbers. The script below does that for one UTC day and exits non-zero when something needs a person, so your scheduler can alert you.
The script
// reconcile.mjs — Node.js 18+. Run nightly for yesterday (UTC), or: node reconcile.mjs 2026-10-23
import { writeFile } from "node:fs/promises";
const API = "https://anytime-capital.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` };
const DAY_MS = 86_400_000;
/** GET with retries on 429 and 5xx (reads are always safe to repeat). */
async function get(path) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`${API}${path}`, { headers });
const body = await res.json().catch(() => ({}));
if (res.ok) return body;
if ((res.status === 429 || res.status >= 500) && attempt < 4) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
continue;
}
throw new Error(`GET ${path}: ${body.error?.code}: ${body.error?.message}`);
}
}
/** Every object of a list, newest first, until `stop` says we've gone back far enough. */
async function* walk(path, params = {}, stop = () => false) {
let cursor = null;
do {
const query = new URLSearchParams({ ...params, limit: "100", ...(cursor ? { cursor } : {}) });
const page = await get(`${path}?${query}`);
for (const item of page.data) {
if (stop(item)) return;
yield item;
}
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
}
const day = process.argv[2] ?? new Date(Date.now() - DAY_MS).toISOString().slice(0, 10);
const from = new Date(`${day}T00:00:00Z`);
const to = new Date(from.getTime() + DAY_MS);
const inDay = (iso) => new Date(iso) >= from && new Date(iso) < to;
const before = (item) => new Date(item.createdAt) < from;
// Payments filter by date on the server; refunds and payouts are newest first, so stop past the day.
const payments = [];
for await (const payment of walk("/payments", { createdFrom: day, createdTo: to.toISOString().slice(0, 10) })) {
payments.push(payment);
}
const refunds = [];
for await (const refund of walk("/refunds", {}, before)) if (inDay(refund.createdAt)) refunds.push(refund);
const payouts = [];
for await (const payout of walk("/payouts", {}, before)) if (inDay(payout.createdAt)) payouts.push(payout);
const balance = await get("/balance");
const paid = payments.filter((payment) => payment.paidAt !== null);
const sum = (items, field) => items.reduce((total, item) => total + (item[field] ?? 0), 0);
const usd = (cents) => (cents / 100).toFixed(2);
const problems = [
...paid.filter((payment) => payment.netCents === null).map((payment) => `${payment.id}: net not reported yet`),
...refunds.filter((refund) => refund.status === "failed").map((refund) => `${refund.id}: refund failed (${refund.failureReason})`),
...payouts
.filter((payout) => ["failed", "reversed", "denied", "canceled"].includes(payout.status))
.map((payout) => `${payout.id}: payout ${payout.status}: ${payout.failureMessage ?? ""}`),
];
// One CSV row per record, for your spreadsheet or accounting import.
const rows = [
["type", "id", "status", "amount_cents", "net_cents", "reference", "created_at"],
...payments.map((p) => ["payment", p.id, p.status, p.amountCents, p.netCents ?? "", p.metadata?.orderId ?? p.clientReference ?? "", p.createdAt]),
...refunds.map((r) => ["refund", r.id, r.status, -r.amountCents, "", r.paymentId, r.createdAt]),
...payouts.map((p) => ["payout", p.id, p.status, p.amountCents ?? "", p.netCents ?? "", p.traceCode ?? "", p.createdAt]),
];
const csv = rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n");
await writeFile(`reconciliation-${day}.csv`, `${csv}\n`);
console.log(`${day}: ${paid.length} payments, $${usd(sum(paid, "amountCents"))} gross, $${usd(sum(paid, "netCents"))} net`);
console.log(`refunds: ${refunds.length}, $${usd(sum(refunds.filter((r) => r.status !== "failed"), "amountCents"))}`);
for (const payout of payouts) {
console.log(`payout ${payout.id} ${payout.status} $${usd(payout.netCents ?? 0)} trace ${payout.traceCode ?? "-"} arrives ${payout.estimatedArrival ?? "-"}`);
}
console.log(`balance now: $${usd(balance.availableCents)} available, $${usd(balance.pendingCents)} pending, $${usd(balance.reserveCents)} reserve`);
if (problems.length > 0) {
console.error(`Needs a look:\n${problems.join("\n")}`);
process.exitCode = 1;
}Run it every night
# 06:15 UTC every day, for the day before
15 6 * * * cd /srv/reconcile && node --env-file=.env reconcile.mjs >> reconcile.log 2>&1Match payouts to your bank
- Each payout's
netCentsis what reaches your bank, andtraceCodeis the trace number your bank can find the deposit by: match on the amount, and on the trace number where your bank shows it. estimatedArrivalsays when to expect it. A payout that iscompletedbut not in your bank a day after that is worth a call to Support.payout.paidandpayout.failedwebhooks tell you as it happens, if you'd rather not wait for the night.