Four public sources, the query that pulled each one, how a trade on a payroll was matched to a line in the comptroller's schedule, and what the comparison cannot tell you.
Article 8 of the New York Labor Law requires contractors on public work to pay the prevailing wage and supplemental benefits for each trade, and to file a certified payroll every week showing each worker's classification, hours and rate. On projects let by New York City agencies the city comptroller sets the schedule and enforces it. On projects let by the state and its authorities, including the Metropolitan Transportation Authority, the state Department of Labor does both.
The state Department of Labor publishes the payrolls filed through its portal as an open dataset. The comptroller publishes its schedule as a PDF each July. This site reads both.
Dataset Certified Payroll Registration: Seven Year Window
(data.ny.gov, id w2zp-sf2x), New York State Department of Labor. Each row is one worker on one
project for one week: contractor (the field is called account), project name and prevailing rate
case number, work category, job title (journeyman or apprentice), straight-time and overtime hours, straight-time
and overtime hourly rates, weekly wages, project address and the department of jurisdiction. Workers are not named.
Despite the title, the file held only 2026 when it was pulled: the earliest week ending in the whole statewide dataset is January 1, 2026 and the latest is September 8, 2026. Statewide it has 1,345,127 rows. The reason is that the portal is new. Labor Law section 220-j required the state to build an electronic certified payroll system by the end of 2025, and contractors have had to file through it only since January 1, 2026, with the first payrolls due January 30. The seven years in the title is how long the state will keep filings, not how far back they go. The file is still filling in: statewide, the number of contractors filing in a month rose from 2,688 in January to 3,247 in July, and the number of projects from 8,488 to 13,104. Filings are due within 30 days plus a 14-day grace period, and the department says it is not yet fining late filers, so the most recent weeks are undercounted.
The portal also does not cover most public construction in the city. The state's filing rules exclude "NYC agency public work projects," which "must use NYC's separate system instead." So work let by the School Construction Authority, the Department of Design and Construction, the Department of Environmental Protection, the Housing Authority and the rest of city government is filed with the city, not here. What is here is the state's share of public work in the five boroughs: the Metropolitan Transportation Authority, the state Department of Transportation, the Dormitory Authority, the Port Authority, the State and City universities, state agencies, and private projects that take public money under section 224-a. Federal projects where only the Davis-Bacon Act applies are excluded too.
The pull (fetch.py) asks the portal for every row whose project ZIP is 10000 to 10499, 11000 to
11499 or 11600 to 11699, or whose project city is a borough name. That returned 92,672 rows. The build
(build.py) then keeps only ZIPs that are in the five boroughs (10000 to 10499, 11004 to 11005, 11100
to 11499, 11690 to 11699) or a borough city name, which drops the Nassau County ZIPs the wide pull caught.
That leaves 76,568 rows. Borough is assigned from ZIP, then from city name if the ZIP is missing.
Of those rows, 55,352 describe a worker-week: they carry a work category. The other 21,216 carry a project, a contractor and a week but no category, hours or rate. They read as weekly statements that no covered work was performed, or as filings the contractor left blank, and they are counted as filings but not as worker-weeks.
Two fields are not trusted. The weekly wages column does not reconcile to hours times rate: among
45,561 worker-weeks with hours, a rate and a wage figure, the wage was within 1 percent of hours times rate
for 0.8 percent of them, and the median ratio was 0.74. In aggregate, though, the column is sound: across
all worker-weeks it sums to $77.19 million, against $77.80 million from hours times the filed rate (overtime
hours at the filed overtime rate, or 1.5 times straight time where none was filed). So the weekly figures
appear to be shuffled or pooled across rows rather than wrong in total. The column is not shown or summed on
the site; the dollar figures below use hours times rate. The straight-time rate field is used as
filed, including 4,718 worker-weeks where it is zero or blank; those are counted but excluded from every
comparison. Thirteen rates above $200 an hour were left in the data as filed.
The Office of the New York City Comptroller
publishes a Construction Worker Prevailing Wage Schedule under Labor Law Article 8 each July. Two are used:
July 1, 2025 to June 30, 2026
for weeks ending before July 1, 2026, and
July 1, 2026 to June 30, 2027
for weeks ending on or after it. Copies as downloaded are in data/source/.
The build reads the text of each PDF and records, for every classification, the effective period, the wage
rate per hour and the supplemental benefit rate per hour. Many trades have several classifications (an
electrician's day, swing and graveyard shifts; a dozen kinds of truck driver; dozens of machines for operating
engineers) and some have rates that step up part way through the year. All of them are kept, and the schedule
tab shows every one: 77 trade sections and 510 rate lines across the two years. Three headings wrap across two
lines in the PDF and defeated the parser; their rates were typed in by hand from the PDF and are marked as such
in build.py: the refrigeration and air-conditioner steamfitter, roadway line striping and the
pointer, waterproofer and caulker.
The comptroller's apprentice schedule is a separate document and is not used. Apprentice worker-weeks (2,027) are shown as counts and excluded from the wage comparison.
Since 2024 a business must hold a certificate from the state Department of Labor's public work contractor
registry to bid on or perform public work. The registry is published as
Contractor Registry Certificate
(data.ny.gov, id i4jv-zkey), 14,574 active certificates when pulled. Each record says whether the
business has been debarred, has outstanding wage assessments, has a final determination for a labor or tax
law violation or a safety violation, is associated with an apprenticeship program, and is a certified minority
or women-owned business.
Contractors were matched by name, after upper-casing, stripping punctuation and dropping corporate suffixes such as Inc., LLC and Corp. 484 of the 486 contractors in the payroll file matched, and in every sampled case the two names are identical, which is what you would expect since both files come from the same portal. The two that did not match are T L D Services Inc and 192 Branch Interior Services, Inc.
Non-Responsible Entities: Beginning 2019
(data.ny.gov, id jhxt-dfv6) is the active list of vendors that a state agency or authority has
found non-responsible under Executive Order 192. It had 18 entries when pulled. None matched a contractor in
the payroll file. The check is kept in the build so a future match will surface.
The two sources do not share a vocabulary. The payroll portal uses the state Department of Labor's trade names
(Laborer - Heavy & Highway, Electrician - Heavy & Highway, Teamster - All Dump Trucks). The comptroller
uses its own (LABORER - HEAVY & HIGHWAY, ELECTRICIAN, DRIVER: TRUCK (TEAMSTER)). A hand-written crosswalk in
build.py, also published as data/crosswalk.json, maps 67 of the 142 payroll categories
found in the city to a comptroller section, and sometimes to particular classifications within it. The 67
cover 50,245 of the 55,352 worker-weeks. Where the match is a judgment call the site says so on the trade row:
the state's building laborer is matched to the comptroller's mason tender because both rates come from the Mason
Tenders District Council agreement; the state's heavy-and-highway electrician is matched to the comptroller's
day-shift Electrician "A" because the comptroller has no separate heavy-and-highway electrician.
Trades left unmatched, with the number of worker-weeks: Laborer - Residential (3,391; the comptroller's construction schedule has no residential rate), Sprinkler Fitter (311; no separate comptroller classification), and 73 smaller categories such as broadband field technician, maintenance electrician and lead abatement painter. They appear on the trade tab with the match switched off.
For each journey-level worker-week with a reported rate in a matched trade, the build finds the comptroller classifications in force on the week-ending date and sorts the reported straight-time rate into one bucket:
Across the 41,191 testable worker-weeks: 3,065 below (7.4 percent), 14,303 at (34.7 percent), 22,448 above (54.5 percent) and 1,375 filed as a package (3.3 percent). The counts row on the front page uses the 44,776 journey-level worker-weeks including the untested range trades as its denominator, which is why its shares read slightly lower.
A rate below the comptroller's wage is a comparison against the city schedule. It is not a finding that the worker was underpaid, for four reasons.
The same is true in the other direction: a rate above the comptroller's wage says nothing about whether the supplemental benefits, which are usually as large as the wage, were paid.
Hours times the filed rate, with overtime at the filed overtime rate, comes to $77.8 million in base wages for the 36 weeks in the file: $65.2 million straight time and $12.6 million overtime. Where a contractor filed the wage-and-benefit package as the rate, the schedule wage was used instead. Applying the lowest matched comptroller supplemental benefit rate to the hours in the 41,191 matched worker-weeks adds an implied $55.1 million in benefits, which is a schedule figure, not a payment the payroll records. By agency, the Metropolitan Transportation Authority accounts for $24.1 million of base wages, the state Department of Transportation $20.0 million and the Port Authority $7.6 million.
The gap on the 3,065 worker-weeks filed below the comptroller's floor, computed as the floor minus the filed rate times the hours, is $805,000, about 1 percent of base wages. It is a comparison against the city schedule, subject to everything in the section above, and it is not a finding of underpayment. The largest shares sit with the state Department of Transportation ($221,000) and the Metropolitan Transportation Authority ($190,000), both state-jurisdiction agencies where the comptroller's schedule does not govern.
For scale: the file shows a median of 129 workers a week on all covered projects in the city, and 116 a week on Metropolitan Transportation Authority projects. The authority committed $15.8 billion of capital work in 2025 alone. The portal is a thin slice of public work in the city, and the totals here describe only that slice.
The department of jurisdiction is free text on the payroll and was filed 619 different ways. The build
collapses obvious variants (NYSDOT, NYS DOT, Department of Transportation) into one name using a list of
patterns in build.py. Anything the patterns do not recognise is shown as filed. "Not stated" means
the field was blank or said unknown.
Run python3 fetch.py to pull the payroll rows, then python3 build.py to read the
schedules, the registry and the non-responsible list from data/source/ and write the JSON the
site reads. The fetch exits with an error if the portal returns fewer than 1,000 lines, so a broken pull cannot
be published as an empty site. Every number on the site is computed in build.py; nothing is
typed in except the three hand-transcribed schedule sections noted above.
The concept, the choice of sources and the design of the comparison were the author's. The code, the PDF parsing and the file handling were done by Anthropic's Claude Code under the author's direction. Checks made along the way: the parsed schedule rates for the mapped trades were read back against the PDF text; the reported-rate modes for the largest trades were checked against the schedule wage by hand (the laborer's $45.60 and $46.60, the electrician's $62.00 and $64.00, the structural ironworker's $58.95 and $59.45); the wage column was tested against hours times rate before being set aside; the registry match was sampled. The risks of AI-assisted work remain. If a number here looks wrong, the source files and the build script are in the repository, and a note would be welcome.