banking.faster-payments-limit
Whether a Faster Payment fits the scheme maximum, the bank's per-payment limit and what is left of its daily limit.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 22 tests, run in TypeScript, Python and Rust.
What it does
Checks a UK Faster Payment against the three limits that can stop it, in the order a payment system applies them:
1. the Faster Payments scheme maximum for a single payment on the payment date (`over-scheme-limit`); 2. the bank's own per-payment limit (`over-transaction-limit`); 3. what is left of the bank's own daily limit after what the customer has already sent today (`over-daily-limit`).
For example
fasterPaymentsCheck(£500.00, £0.00, £25,000.00, £25,000.00, 2026-09-23)→ decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £25,000.00 an ordinary payment within every limitfasterPaymentsCheck(£25,000.00, £0.00, £25,000.00, £50,000.00, 2026-09-23)→ decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £50,000.00 exactly the per-payment limit is allowedfasterPaymentsCheck(£25,000.01, £0.00, £25,000.00, £50,000.00, 2026-09-23)→ decision over-transaction-limit, allowed false, scheme limit £1,000,000.00, remaining today £50,000.00 a penny over the per-payment limit
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function fasterPaymentsCheck(amount: Money, sentToday: Money, perTransactionLimit: Money, dailyLimit: Money, onDate: string): FasterPaymentsCheck
| amount | Money | the payment being made, GBP, more than zero |
| sentToday | Money | what the customer has already sent today, counted against dailyLimit |
| perTransactionLimit | Money | the bank's own limit for one payment |
| dailyLimit | Money | the bank's own limit for a day's payments |
| onDate | date | the payment date; the scheme maximum is looked up for it |
| returns | FasterPaymentsCheck |
The types it declares, generated into your project
export type FpsDecision = "allowed" | "over-scheme-limit" | "over-transaction-limit" | "over-daily-limit";
/** The answer, and the limits that produced it. */
export interface FasterPaymentsCheck {
/** the first limit the payment breaks, or allowed */
readonly decision: FpsDecision;
readonly allowed: boolean;
/** the Faster Payments scheme maximum on onDate */
readonly schemeLimit: Money;
/** dailyLimit less sentToday, never below zero, before this payment */
readonly remainingToday: Money;
}
Your code names it in one line, in the file that uses it
import { fasterPaymentsCheck } from "#fune/banking.faster-payments-limit@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { SCHEME_LIMITS, SCHEME_LIMITS_HISTORY } from "./banking_faster_payments_limit_data.ts"; ← this capability’s own data, compiled from data/scheme-limits.json into the same file by fune build
import { type FasterPaymentsCheck, type FpsDecision } from "./banking_faster_payments_limit_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
/** The scheme maximum per payment in force on a date, in pence. */
function schemeLimitOn(onDate: string): number {
let best: { limitMinor: number; validFrom: string } | null = null;
for (const row of SCHEME_LIMITS) {
if (onDate < row.validFrom) continue;
if (row.validTo !== null && onDate > row.validTo) continue;
if (best === null || row.validFrom > best.validFrom) best = row;
}
if (best !== null) return best.limitMinor;
let earliest: string | null = null;
for (const row of SCHEME_LIMITS) {
if (earliest === null || row.validFrom < earliest) earliest = row.validFrom;
}
// A build installed with history=current only carries today's limit, and
// answering an old date with it would be exactly the wrong answer.
if (SCHEME_LIMITS_HISTORY !== "full" && earliest !== null && onDate < earliest) {
throw new RangeError(
`no Faster Payments scheme limit on ${onDate}: this build was installed with history=${SCHEME_LIMITS_HISTORY}, ` +
`so it only carries limits from ${earliest}. Reinstall with history=full to check older payments.`
);
}
throw new RangeError(`no Faster Payments scheme limit on ${onDate}: the scheme opened on ${earliest}`);
}
/**
* Check a Faster Payment against three limits, in order: the scheme maximum
* on the payment date, the bank's per-payment limit, and what is left of the
* bank's daily limit. The decision names the first one broken.
*/
export function fasterPaymentsCheck(
amount: Money,
sentToday: Money,
perTransactionLimit: Money,
dailyLimit: Money,
onDate: string,
): FasterPaymentsCheck {
if (!ISO_DATE.test(onDate)) {
throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
}
for (const m of [amount, sentToday, perTransactionLimit, dailyLimit]) {
if (m.currency !== "GBP") {
throw new RangeError(`Faster Payments is a sterling scheme: received ${m.currency}`);
}
}
if (amount.minor <= 0) {
throw new RangeError(`amount must be greater than zero, received ${amount.minor}`);
}
if (sentToday.minor < 0 || perTransactionLimit.minor < 0 || dailyLimit.minor < 0) {
throw new RangeError("sentToday and the limits must not be negative");
}
const scheme = schemeLimitOn(onDate);
const remaining = Math.max(0, dailyLimit.minor - sentToday.minor);
let decision: FpsDecision = "allowed";
if (amount.minor > scheme) decision = "over-scheme-limit";
else if (amount.minor > perTransactionLimit.minor) decision = "over-transaction-limit";
else if (amount.minor > remaining) decision = "over-daily-limit";
return {
decision,
allowed: decision === "allowed",
schemeLimit: money(scheme, "GBP"),
remainingToday: money(remaining, "GBP"),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add banking.faster-payments-limit
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./banking.faster-payments-limit-1.0.0-typescript.fune, or fetch it from a terminal with fune pull banking.faster-payments-limit@1.0.0:typescript.
The whole function, every language, is one file too: banking.faster-payments-limit-1.0.0.fune, 32,784 bytes, sha256 b96e25a3af5950d0b2582501c0bd301d16c5e1e3b8fc68dc31b6d7de8454e987. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before banking.faster-payments-limit
after — your function gets the result and the arguments, and returns the final result.
// fune: after banking.faster-payments-limit
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace money.amount in banking.faster-payments-limit
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show banking.faster-payments-limit --steps.
// fune: step banking.faster-payments-limit after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| an ordinary payment within every limit | £500.00, £0.00, £25,000.00, £25,000.00, 2026-09-23 | → | decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £25,000.00 |
| exactly the per-payment limit is allowed | £25,000.00, £0.00, £25,000.00, £50,000.00, 2026-09-23 | → | decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £50,000.00 |
| a penny over the per-payment limit | £25,000.01, £0.00, £25,000.00, £50,000.00, 2026-09-23 | → | decision over-transaction-limit, allowed false, scheme limit £1,000,000.00, remaining today £50,000.00 |
| exactly what is left of the daily limit is allowed | £10,000.00, £15,000.00, £25,000.00, £25,000.00, 2026-09-23 | → | decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £10,000.00 |
| a penny over what is left today | £10,000.01, £15,000.00, £25,000.00, £25,000.00, 2026-09-23 | → | decision over-daily-limit, allowed false, scheme limit £1,000,000.00, remaining today £10,000.00 |
| already over the daily limit: nothing is left, never negative | £1.00, £30,000.00, £25,000.00, £25,000.00, 2026-09-23 | → | decision over-daily-limit, allowed false, scheme limit £1,000,000.00, remaining today £0.00 |
| the scheme maximum of one million pounds | £1,000,000.00, £0.00, £2,000,000.00, £2,000,000.00, 2026-09-23 | → | decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £2,000,000.00 |
| over the scheme maximum even when the bank allows more | £1,000,000.01, £0.00, £2,000,000.00, £2,000,000.00, 2026-09-23 | → | decision over-scheme-limit, allowed false, scheme limit £1,000,000.00, remaining today £2,000,000.00 |
| the scheme limit is checked before the bank's own limits | £1,000,000.01, £0.00, £1.00, £1.00, 2026-09-23 | → | decision over-scheme-limit, allowed false, scheme limit £1,000,000.00, remaining today £1.00 |
| the per-payment limit is checked before the daily limit | £30,000.00, £0.00, £25,000.00, £10,000.00, 2026-09-23 | → | decision over-transaction-limit, allowed false, scheme limit £1,000,000.00, remaining today £10,000.00 |
Show the other 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| £300,000 was over the scheme limit in 2021 | £300,000.00, £0.00, £1,000,000.00, £1,000,000.00, 2021-06-01 | → | decision over-scheme-limit, allowed false, scheme limit £250,000.00, remaining today £1,000,000.00 |
| the day before the £1m limit | £300,000.00, £0.00, £1,000,000.00, £1,000,000.00, 2022-02-09 | → | decision over-scheme-limit, allowed false, scheme limit £250,000.00, remaining today £1,000,000.00 |
| the day the £1m limit was announced | £300,000.00, £0.00, £1,000,000.00, £1,000,000.00, 2022-02-10 | → | decision allowed, allowed true, scheme limit £1,000,000.00, remaining today £1,000,000.00 |
| £250,000 from 10 November 2015 | £250,000.00, £0.00, £1,000,000.00, £1,000,000.00, 2015-11-10 | → | decision allowed, allowed true, scheme limit £250,000.00, remaining today £1,000,000.00 |
| £100,000 the day before | £250,000.00, £0.00, £1,000,000.00, £1,000,000.00, 2015-11-09 | → | decision over-scheme-limit, allowed false, scheme limit £100,000.00, remaining today £1,000,000.00 |
| £10,000 at launch | £10,000.01, £0.00, £1,000,000.00, £1,000,000.00, 2008-05-27 | → | decision over-scheme-limit, allowed false, scheme limit £10,000.00, remaining today £1,000,000.00 |
| a zero daily limit blocks everything | £0.01, £0.00, £25,000.00, £0.00, 2026-09-23 | → | decision over-daily-limit, allowed false, scheme limit £1,000,000.00, remaining today £0.00 |
| before the scheme opened is an error | £1.00, £0.00, £1.00, £1.00, 2008-05-26 | → | error: no Faster Payments scheme limit on 2008-05-26 |
| a euro payment is an error | €1.00, £0.00, £1.00, £1.00, 2026-09-23 | → | error: Faster Payments is a sterling scheme |
| a zero payment is an error | £0.00, £0.00, £1.00, £1.00, 2026-09-23 | → | error: amount must be greater than zero |
| a negative daily limit is an error | £1.00, £0.00, £1.00, -£0.01, 2026-09-23 | → | error: must not be negative |
| a malformed date is an error | £1.00, £0.00, £1.00, £1.00, 23/09/2026 | → | error: onDate must be an ISO date |
More from the author
The decision names the first limit broken, or `allowed`. `remainingToday` is the daily limit less what was sent today, never below zero, before this payment, so a UI can say "you can send up to £X more today".
## Whose limits
The per-payment and daily limits are the caller's: each bank sets its own, usually far below the scheme maximum and different by channel and account type. Pay.UK says as much: individual payment service providers decide what their customers can send within the scheme limit. Only the scheme maximum is data here, because it is a published rule that has changed over time.
## The scheme maximum, as dated data
`data/scheme-limits.json`, one row per limit with `validFrom`/`validTo`:
| From | Limit | Source | |------|-------|--------| | 2008-05-27 | £10,000 | Scheme launch. Secondary sources only (e.g. The Global Treasurer, "UK Faster Payments: the end of the beginning?", 2010-12-08). | | 2010-09-06 | £100,000 | Secondary sources only (The Global Treasurer, as above), which describe it as the Direct Corporate Access limit. | | 2015-11-10 | £250,000 | Faster Payments Scheme press release "Faster Payments increases scheme limit to £250,000", reproduced at https://www.finextra.com/pressarticle/62073/faster-payments-increases-scheme-limit-to-250000 | | 2022-02-10 | £1,000,000 | Pay.UK press release "£1 million Faster Payments now possible", 10 February 2022, https://newseventsinsights.wearepay.uk/media-centre/press-releases/1-million-faster-payments-now-possible/ ; Pay.UK "Transaction limits", https://www.wearepay.uk/what-we-do/payment-systems/faster-payment-system/transaction-limits/ |
Honesty about the dates: Pay.UK's press release says £1m payments are "now possible" and gives no switch-on day, so the row starts on the release date; it previously said the limit was £250,000. The 2008 and 2010 rows could not be checked against a primary Faster Payments or Pay.UK source and should be confirmed before relying on this for payments from those years. Before a row changes, publish a new version with the corrected data.
A date before the scheme opened is an error. The data declares its effective columns, so a project can install `history=current`; such a build refuses a date before its earliest surviving limit rather than answering it with today's £1m.
## Edge cases
- Sterling only: every amount must be GBP. - The amount must be greater than zero; `sentToday` and the limits must not be negative. Limits are inclusive: a payment of exactly the limit is allowed.
Files
| Path | Bytes |
|---|---|
| README.md | 2,967 |
| data/scheme-limits.json | 821 |
| impl/python.py | 3,161 |
| impl/rust.rs | 4,308 |
| impl/typescript.ts | 3,044 |
| vectors.json | 12,767 |