Functional Weave
Code in Rust

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.1 · published 2026-10-03 by charlie · Anterra

Pinned by 24 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

  • faster_payments_check(£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 limit
  • faster_payments_check(£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 allowed
  • faster_payments_check(£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.

pub fn faster_payments_check(amount: &Money, sent_today: &Money, per_transaction_limit: &Money, daily_limit: &Money, on_date: &str) -> FasterPaymentsCheck
amountMoneythe payment being made, GBP, more than zero
sent_todayMoneywhat the customer has already sent today, counted against dailyLimit
per_transaction_limitMoneythe bank's own limit for one payment
daily_limitMoneythe bank's own limit for a day's payments
on_datedatethe payment date; the scheme maximum is looked up for it
returnsFasterPaymentsCheck

The types it declares, generated into your project

// FpsDecision is a string in Rust, one of: "allowed", "over-scheme-limit", "over-transaction-limit", "over-daily-limit".
// Parameters take it as &str and results hold it as String.

/// The answer, and the limits that produced it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FasterPaymentsCheck {
    /// the first limit the payment breaks, or allowed
    pub decision: String,
    pub allowed: bool,
    /// the Faster Payments scheme maximum on onDate
    pub scheme_limit: Money,
    /// dailyLimit less sentToday, never below zero, before this payment
    pub remaining_today: Money,
}

Your code names it in one line, in the file that uses it

fune!(banking.faster-payments-limit@^1);  // then call faster_payments_check(…)
impl/rust.rs · 117 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::banking_faster_payments_limit_data::{SCHEME_LIMITS, SCHEME_LIMITS_HISTORY};  ← this capability’s own data, compiled from data/scheme-limits.json into the same file by fune build
use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

/// The scheme maximum per payment in force on a date, in pence.
fn scheme_limit_on(on_date: &str) -> i64 {
    let mut best: Option<(i64, &'static str)> = None;
    for row in SCHEME_LIMITS {
        if on_date < row.valid_from {
            continue;
        }
        if let Some(valid_to) = row.valid_to {
            if on_date > valid_to {
                continue;
            }
        }
        if best.map_or(true, |(_, from)| row.valid_from > from) {
            best = Some((row.limit_minor, row.valid_from));
        }
    }
    if let Some((limit, _)) = best {
        return limit;
    }
    let mut earliest: Option<&'static str> = None;
    for row in SCHEME_LIMITS {
        if earliest.map_or(true, |e| row.valid_from < e) {
            earliest = Some(row.valid_from);
        }
    }
    let earliest = earliest.unwrap_or("");
    // 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.is_empty() && on_date < earliest {
        panic!(
            "no Faster Payments scheme limit on {}: this build was installed with history={}, so it only carries limits from {}. Reinstall with history=full to check older payments.",
            on_date, SCHEME_LIMITS_HISTORY, earliest
        );
    }
    panic!("no Faster Payments scheme limit on {}: the scheme opened on {}", on_date, 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.
///
/// # Panics
/// Panics on a malformed date, a date before the scheme opened, a currency
/// other than GBP, an amount of zero or less, or a negative limit.
pub fn faster_payments_check(
    amount: &Money,
    sent_today: &Money,
    per_transaction_limit: &Money,
    daily_limit: &Money,
    on_date: &str,
) -> FasterPaymentsCheck {
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    for m in [amount, sent_today, per_transaction_limit, daily_limit] {
        if m.currency != "GBP" {
            panic!("Faster Payments is a sterling scheme: received {}", m.currency);
        }
    }
    if amount.minor <= 0 {
        panic!("amount must be greater than zero, received {}", amount.minor);
    }
    if sent_today.minor < 0 || per_transaction_limit.minor < 0 || daily_limit.minor < 0 {
        panic!("sentToday and the limits must not be negative");
    }
    let scheme = scheme_limit_on(on_date);
    let remaining = (daily_limit.minor - sent_today.minor).max(0);
    let decision = if amount.minor > scheme {
        "over-scheme-limit"
    } else if amount.minor > per_transaction_limit.minor {
        "over-transaction-limit"
    } else if amount.minor > remaining {
        "over-daily-limit"
    } else {
        "allowed"
    };
    FasterPaymentsCheck {
        decision: decision.to_string(),
        allowed: decision == "allowed",
        scheme_limit: money(scheme, "GBP"),
        remaining_today: money(remaining, "GBP"),
    }
}

pub fn faster_payments_check_to_value(check: &FasterPaymentsCheck) -> Value {
    Value::obj(vec![
        ("decision", Value::str(&check.decision)),
        ("allowed", Value::Bool(check.allowed)),
        ("schemeLimit", money_to_value(&check.scheme_limit)),
        ("remainingToday", money_to_value(&check.remaining_today)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    faster_payments_check_to_value(&faster_payments_check(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &money_from_value(&args[2]),
        &money_from_value(&args[3]),
        args[4].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add banking.faster-payments-limit
Download for Rust banking.faster-payments-limit-1.0.1-rust.fune · 27,384 bytes sha256 471e376c47eb6df10a0e26029f7c49e6d86d455dcd2d9cb0e5f41b00c4021617

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./banking.faster-payments-limit-1.0.1-rust.fune, or fetch it from a terminal with fune pull banking.faster-payments-limit@1.0.1:rust.

The whole function, every language, is one file too: banking.faster-payments-limit-1.0.1.fune, 33,854 bytes, sha256 fdef054961ba90af897b02b441c5d846806e7f054dc87045f8a1a856581ef746. 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.

CaseArgumentsExpected
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 14 tests
CaseArgumentsExpected
£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
a trailing newline after onDate is not an ISO date £1.00, £0.00, £1.00, £1.00, 2026-09-16 → error: onDate must be an ISO date
Arabic-Indic digits in onDate are not an ISO date £1.00, £0.00, £1.00, £1.00, ٢٠٢٦-09-16 → 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.

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.

Files

PathBytes
README.md3,059
data/scheme-limits.json821
impl/python.py3,172
impl/rust.rs4,308
impl/typescript.ts3,044
vectors.json13,615