Deposit and withdrawal approvals for an investment platform admin panel. Decides what can clear automatically and what a human has to look at, and moves requests between states without letting them go somewhere they should not.
Zero dependencies, one file, runs anywhere ES modules run.
Every platform starts with manual approval, which is correct. Then the queue gets to two
hundred a day and somebody adds an auto approve, usually as an if amount < x in a
controller, and the rules end up scattered across four files with no record of which one
fired.
This keeps the decision in one place, gives you the reasons back so the panel can show the operator why a request is in front of them, and refuses the state changes that break a ledger. An approved request cannot be approved again. A rejection without a reason is an error, not a nullable column.
pending -> approved | rejected | held
held -> approved | rejected
approved -> nothing
rejected -> nothing
Held is the one people leave out. Without it your only options are to pay something that looks wrong or to reject a customer you have not finished checking, and you will end up doing neither and letting it sit in pending with no marker on it.
import { createRequest, evaluate } from './approvals.js';
const request = createRequest({
id: 'w-4192',
userId: 'u-88',
kind: 'withdrawal',
amount: 2500, // 25.00 in minor units
address: 'bc1qexample',
});
evaluate(request, {
autoApproveUnder: 5000,
holdAbove: 500000,
dailyUserLimit: 100000,
velocity: { count: 3, windowMinutes: 60 },
}, { now: new Date(), history: settledRequests });Comes back as one of three actions with the reasons attached:
{ action: 'review', reasons: ['address-not-used-before'] }autoclears without a humanreviewgoes to the normal queueholdgoes to the queue flagged, something looks off
The worst outcome wins. A request that trips both a review rule and a hold rule is held.
| Policy | What it does |
|---|---|
autoApproveUnder |
anything at or over this goes to a human |
holdAbove |
large amounts are flagged rather than merely queued |
firstRequestIsManual |
a user with nothing approved yet is always looked at, on by default |
newAddressIsManual |
first withdrawal to an unseen address is looked at, on by default |
requireProof |
deposits with no payment proof attached go to a human, on by default |
dailyUserLimit |
counts what the user already had approved in the last 24 hours |
velocity |
too many requests inside a window gets held |
manualOnly |
switch the whole thing off, everything goes to a human |
import { process } from './approvals.js';
const { approved, review, held } = process(queue, policy, { now, history }, { actor: 'policy' });Automatic approvals are applied and everything else comes back untouched, sorted into the two piles a person works from. Anything cleared inside the batch becomes history for the requests after it, so a user cannot slide three small withdrawals past a daily limit by sending them together.
Automatic approvals are recorded with the actor you pass in, policy by default, so the
audit trail does not pretend a person was sitting there.
import { transition, APPROVED, REJECTED } from './approvals.js';
const done = transition(request, { to: APPROVED, actor: 'anthony' });
const no = transition(request, { to: REJECTED, actor: 'anthony', reason: 'address does not match the one on file' });Returns a new object and appends to audit. Credit the balance on the result of this call,
never before it.
summarise(requests);
// { pending, held, approved, rejected, openCount, openAmount, approvedAmount }
waiting(requests, { targetMinutes: 60 });
// { oldestMinutes, breached, queue }openAmount is money you have promised and not yet sent. breached is everything past the
window you advertise, oldest first, which is the list worth working before anything else.
No balances, no wallets, no sending. It decides and it records. Actually moving money is the part that should stay in your own code where you can see it.
node test.js
Twenty five of them, no framework.
Written while building investment platform software at investmentscript.com. The panel these rules were pulled out of is walked through screen by screen in the admin panel overview, including the withdrawal queue and the deposit proof step.
MIT licensed.