Compute ACH settlement dates, same-day windows and return deadlines using the real Federal Reserve calendar.
ACH code usually defines a business day as "a weekday that is not a federal holiday". The Federal Reserve does not work that way. When a holiday falls on a Saturday, the federal government closes the Friday before but the Fed stays open. So Friday 2021-12-31 was a banking day, and so will be 2027-12-24 and 2027-12-31. Juneteenth was a federal holiday in 2021 but did not close the Fed until 2022. Same-day deadlines are in Eastern Time, which moves with DST, while most submission timestamps are in UTC. Get any of this wrong and you compute the wrong settlement date, miss a return deadline, or fund an account on the wrong day. NACHA file validators check the 94-character records. They do not check the dates, and generic business-day libraries use the government calendar.
The library has four small modules. None of them needs a time zone database or a network call.
Dates as integers (src/civil.ts). A calendar date is a day number, meaning days since 1970-01-01, converted with Howard Hinnant's days_from_civil / civil_from_days algorithms. Adding days and finding weekdays is integer arithmetic, and the host time zone never affects the result.
The Fed calendar is a set of rules, not a list of dates (src/fedcal.ts). Each of the eleven holidays is stored as data: a fixed date, the nth weekday of a month, or the last weekday of a month, plus the first year the Fed closed for it. Observance is computed from those rules. A holiday on a Sunday closes the Fed on Monday. A holiday on a Saturday is not observed. A banking day is a weekday with no observed holiday. The same rules, run with the government's Saturday-to-Friday shift, list the days when the government is closed and the Fed is open. Supported years are 1987-2199.
Eastern Time from the statute (src/eastern.ts). DST starts on the first Sunday in April and ends on the last Sunday in October for 1987-2006. From 2007 it runs from the second Sunday in March to the first Sunday in November. Both switches happen at 02:00 local time. A property test checks the conversion against the host's America/New_York tz data on 4,000 random instants between 1990 and 2100.
Settlement is a small state machine with a trace (src/settle.ts). The submission instant is converted to Eastern Time, then placed in a FedACH processing cycle:
02:15 ET last same-day deadline (16:45 ET since 2021-03-19)
banking day D ─────┼──────────────────────────────┼──────────────────▶
overnight cycle │ daytime cycle on D │ overnight cycle
for D, settles │ 10:30 → settles 13:00 │ for the next banking
08:30 ET on D │ 14:45 → settles 17:00 │ day, settles 08:30 ET
│ 16:45 → settles 18:00 │
A file submitted on a weekend or holiday goes to the overnight cycle of the next banking day. Next, the Effective Entry Date is read. For a six-digit YYMMDD date, the century chosen is the one that puts the date nearest the submission date. If that date is not a banking day, it moves forward to the next banking day. An effective date later than the cycle day settles on that date at 08:30 ET. Otherwise the entry settles as early as it can. In the daytime cycle, that means the first same-day window whose deadline it meets. It needs to be eligible, though: not IAT, and within the per-entry limit on that date ($25,000 from 2016, $100,000 from 2020-03-20, $1,000,000 from 2022-03-18). If it is not eligible, it settles on the next banking day. An effective date that is stale or not a real calendar date settles at the earliest opportunity, just as the Fed handles it. Each decision adds a named step to the trace, so every answer comes with its reason.
Return deadlines (src/returns.ts). Each supported R-code maps to a timeframe. Most codes must reach the ODFI by opening of business on the second banking day after settlement. Unauthorized-debit and check-conversion codes (R05, R07, R10, R11, R33, R37, R38, R51-R53) are due on the banking day after the 60th calendar day. R06, R23 and R31 have no deadline counted from settlement.
Worked example: a $250,000 PPD file is submitted at 19:44 UTC on the day before Thanksgiving 2026, with effective date 261126. In EST that is 14:44, during the daytime cycle. But the effective date is Thanksgiving, which moves to Friday 11-27. That is after the cycle day, so the entry settles Friday at 08:30 ET, not same-day on Wednesday.
Requires Node.js 20 or later.
git clone <this repository> achclock && cd achclock
npm install # also compiles TypeScript to dist/ via the prepare script
npm test
Run the CLI with node dist/src/cli.js. After npm link it is also available as achclock.
$ node dist/src/cli.js settle --submitted 2026-11-25T19:44:00Z --effective 261126 --amount 250000 --sec PPD
Submitted: 2026-11-25T19:44:00Z (2026-11-25T14:44:00-05:00 EST)
Effective date: 2026-11-26
Settlement date: 2026-11-27 (Fri)
Window: next-day, settles 08:30 ET
Trace:
[eastern-time] submitted 2026-11-25T19:44:00Z = 2026-11-25T14:44:00-05:00 EST (2007+ rule: second Sunday in March to first Sunday in November)
[daytime-cycle] received during the daytime cycle on banking day 2026-11-25
[effective-date] YYMMDD 261126 read as 2026-11-26 (century nearest the submission date)
[effective-date-not-banking-day] 2026-11-26 is a Fed holiday: Thanksgiving Day (fourth Thursday in November, rule in force since 1942); rolls forward to 2026-11-27
[forward-dated] effective date is after 2026-11-25; settles on the effective date at 08:30 ET
The day after DST ends, 19:46 UTC is 14:46 EST. That misses the 14:45 window, so the entry lands in the 16:45 window:
$ node dist/src/cli.js settle --submitted 2026-11-02T19:46:00Z --effective 261102 --amount 2500 --sec WEB
Submitted: 2026-11-02T19:46:00Z (2026-11-02T14:46:00-05:00 EST)
Effective date: 2026-11-02
Settlement date: 2026-11-02 (Mon)
Window: same-day 16:45 ET deadline, settles 18:00 ET
Trace:
[eastern-time] submitted 2026-11-02T19:46:00Z = 2026-11-02T14:46:00-05:00 EST (2007+ rule: second Sunday in March to first Sunday in November)
[daytime-cycle] received during the daytime cycle on banking day 2026-11-02
[effective-date] YYMMDD 261102 read as 2026-11-02 (century nearest the submission date)
[same-day-window] eligible (WEB, $2,500.00 within $1,000,000.00); met the 16:45 ET deadline, settles 18:00 ET
Return deadlines:
$ node dist/src/cli.js return --settled 2026-12-24 --code R01
Return code: R01 Insufficient funds
Timeframe: 2-banking-days
Settlement date: 2026-12-24
Deadline: 2026-12-29 (Tue), opening of business
Trace:
[return-code] R01 Insufficient funds: 2-banking-days
[two-banking-days] second banking day after settlement 2026-12-24 is 2026-12-29
$ node dist/src/cli.js return --settled 2026-11-27 --code R10
Return code: R10 Customer advises originator is not known and/or not authorized
Timeframe: 60-calendar-days
Settlement date: 2026-11-27
Deadline: 2027-01-27 (Wed), opening of business
Trace:
[return-code] R10 Customer advises originator is not known and/or not authorized: 60-calendar-days
[sixty-calendar-days] 60th calendar day after settlement is 2027-01-26
[following-banking-day] available by opening of business on the next banking day after it: 2027-01-27
The holiday calendar, including the days when the Fed and the government disagree:
$ node dist/src/cli.js holidays --year 2027
Federal Reserve holidays 2027
New Year's Day 2027-01-01 Fri
Birthday of Martin Luther King, Jr. 2027-01-18 Mon
Washington's Birthday 2027-02-15 Mon
Memorial Day 2027-05-31 Mon
Juneteenth National Independence Day not observed (Saturday)
Independence Day 2027-07-05 Mon
Labor Day 2027-09-06 Mon
Columbus Day 2027-10-11 Mon
Veterans Day 2027-11-11 Thu
Thanksgiving Day 2027-11-25 Thu
Christmas Day not observed (Saturday)
Federal government closed, Federal Reserve open:
2027-06-18 Fri Juneteenth National Independence Day: 2027-06-19 is a Saturday: the government closes the Friday before, the Fed does not
2027-12-24 Fri Christmas Day: 2027-12-25 is a Saturday: the government closes the Friday before, the Fed does not
2027-12-31 Fri New Year's Day: 2028-01-01 is a Saturday: the government closes the Friday before, the Fed does not
$ node dist/src/cli.js day 2022-06-20
2022-06-20 is a Fed holiday: Juneteenth National Independence Day (June 19, rule in force since 2022; falls on Sunday 2022-06-19, observed Monday)
Add --json to any command for machine-readable output. Bad input exits with code 1 and a message, for example achclock: expected an ISO 8601 instant with Z or an offset, got "2026-11-25T14:44:00". A missing or malformed flag exits with code 2 and prints the usage text.
As a library:
import { settle, returnDeadline, isBankingDay, parseIsoDate } from "achclock";
const s = settle({ submitted: "2026-11-25T19:44:00Z", effective: "261126", amount: 250000, sec: "PPD" });
s.settlementDate; // "2026-11-27"
s.window; // "next-day"
returnDeadline(s.settlementDay, "R01").deadline; // "2026-12-01"
isBankingDay(parseIsoDate("2021-12-31")); // trueThe project claims correctness, not speed, so this section reports the checks that back that claim. npm test runs 81 tests with Node's built-in runner. On an Apple Silicon Mac with Node 24, the whole suite runs in about 0.2 s.
-
Calendar. The observed closure dates are checked against the Fed's published holiday list for recent years, including the cases that break naive code: Saturday holidays (2021-12-31 and 2022-12-26), the first Juneteenth closure in 2022 and the 2021 date that was still open, and Sunday holidays observed on Monday.
-
Same-day windows. Tests cover the minute before and after each deadline, on the DST transition Sundays and the Mondays after them in 2025 and 2026. They also check the per-entry limit at exactly $1,000,000.00 and one cent above it, and the window and limit schedules in force on past dates.
-
Seeded property tests (
test/properties.test.ts), with 4,000 random cases each over 1990-2100:- every settlement date is a banking day, never before the Eastern submission date, and only same-day when it falls on the submission date;
- a later submission of the same entry never settles earlier;
addBankingDays(d, n)is strictly increasing in n;- return deadlines are banking days on or after settlement;
- the DST-rule Eastern conversion matches the host tz database on every sampled instant.
These tests do not shrink. A failure prints the seed and the exact input at once. Rerun one with
ACHCLOCK_SEED=<n> npm test.
The main decision was to encode the rules, not the dates. The Fed publishes a holiday table only a few years ahead, and most libraries copy that table. That works until the table runs out, and it loses the reason behind each date. Here every holiday carries its rule, its first year and its observance logic, and each answer arrives with a trace naming the rule that produced it. The cost is that a one-off closure, or a future change to the rules, needs a code change, not a data refresh. For a tool meant to explain dates, being able to show why a date was chosen seemed worth more than being editable as data.
The same reasoning applies to Eastern Time. The DST statute is two rules, so implementing it directly makes the result independent of the ICU data a given Node build ships with. The tz database is still used, but only as a test oracle. The settlement API also refuses timestamps with no offset. Guessing a zone is exactly the mistake this tool exists to prevent, so an error is better than a silently wrong window. Two-digit years are resolved to the century nearest the submission date, not to a fixed pivot year, so December files dated for early January roll into the new year correctly.
- It models the Federal Reserve's FedACH schedule. The Clearing House's EPN network, and the earlier cutoffs that ODFIs set for their customers, are not modelled. The deadlines here are the latest the operator accepts, not the time your bank needs the file.
- Unscheduled Fed closures, such as a national day of mourning, are not included. Neither are future changes to holidays, windows or limits.
- Supported years are 1987-2199. Same Day ACH rules begin on 2016-09-23. Before that date every entry settles next-day.
- It computes settlement for forward entries only. It does not parse NACHA files, compute funds-availability times for the RDFI, or model the rules for dishonored and contested returns.
- The R-code table sorts each code into one of three timeframes. The NACHA Rules attach further conditions to some codes, and those are not modelled. For example, R06 and R31 depend on the ODFI agreeing, and whether R05 or R10 applies depends on the SEC code and account type. The tool returns the timeframe for the code you pass in and does not check that the code fits the entry.
- Same-day eligibility checks SEC code and amount only. It does not check whether a particular RDFI or ODFI takes part.
MIT. See LICENSE.