From 24ff3fe77bb3a80baa65386a67575079e777dea7 Mon Sep 17 00:00:00 2001 From: azender1 Date: Thu, 24 Sep 2026 22:07:00 -0400 Subject: [PATCH 1/2] Add buyer-gated product agent and local fringe proof pilot --- PRODUCT_AGENT.md | 77 +++++++ products/fringe_proof/.gitignore | 2 + products/fringe_proof/README.md | 62 ++++++ products/fringe_proof/fixtures/fund_ack.csv | 2 + products/fringe_proof/fixtures/payroll.csv | 4 + products/fringe_proof/fixtures/rates.csv | 4 + products/fringe_proof/fixtures/remittance.csv | 3 + products/fringe_proof/reconcile.py | 195 ++++++++++++++++++ products/fringe_proof/test_reconcile.py | 79 +++++++ 9 files changed, 428 insertions(+) create mode 100644 PRODUCT_AGENT.md create mode 100644 products/fringe_proof/.gitignore create mode 100644 products/fringe_proof/README.md create mode 100644 products/fringe_proof/fixtures/fund_ack.csv create mode 100644 products/fringe_proof/fixtures/payroll.csv create mode 100644 products/fringe_proof/fixtures/rates.csv create mode 100644 products/fringe_proof/fixtures/remittance.csv create mode 100644 products/fringe_proof/reconcile.py create mode 100644 products/fringe_proof/test_reconcile.py diff --git a/PRODUCT_AGENT.md b/PRODUCT_AGENT.md new file mode 100644 index 0000000..5a49c6f --- /dev/null +++ b/PRODUCT_AGENT.md @@ -0,0 +1,77 @@ +# SafeAgent product agent: operating contract + +Goal: find an existing, funded, costly operational problem where SafeAgent's +cross-source evidence and execution receipts add value. The current first +hypothesis is **independent union fringe remittance proof** for contractors. +Do not describe a market as validated until an outside operator supplies +authorized records, confirms a previously unknown actionable discrepancy or +meaningful time saved, and pays for continued use. + +## Buyer loop (daily research) + +1. Search public operator and buyer sources: contractor finance associations, + benefit fund audit forums, published procurement requests, named customer + accounts, and vendor case studies. Record source date, URL, exact firsthand + problem, current spend, buyer role, existing workaround, and whether the + writer is an operator, a vendor, or another tool builder. +2. Score a candidate higher for a current paid workaround and a record-backed + cross-system discrepancy; lower for generic interest or an easily solved + spreadsheet/check-constraint problem. Avoid contacts without a visible + budget owner or means to inspect real records. +3. Propose at most three named, verifiable buyer candidates per run. Describe + a specific no-call, read-only pilot for each and an explicit reason the + existing tools might already solve it. State when there are none. +4. Never send mail, messages, invitations, or public comments as part of + scouting. Never scrape private contacts or use employer data. Surface + draft outreach for review, with exact recipient identity and source URL. + +## Build loop (weekly) + +1. Re-evaluate product direction using buyer evidence, including evidence + against the current hypothesis. Prefer a small supported change over + additional generic SafeAgent features. Existing software such as + LaborAid and LCPcertified already performs important validation. +2. Reproduce a real, bounded discrepancy with synthetic or authorized data. + Keep inputs local and read-only. Confirm explicit fund-specific calculation + and rounding rules with a domain operator; never infer them from marketing. +3. Work on a new branch based on current `main`, run a meaningful regression, + and open a draft PR with the observed failure, behavior change, tests, + limitations, and buyer evidence. Never merge or deploy automatically. +4. Review the operating contract itself when evidence changes the priorities; + propose edits in the same draft PR with reasons and an exit criterion. +5. Stop the build for that run if no new buyer evidence or reproducible failure + supports a change. Report the finding instead of generating busywork. + +## Product acceptance gates + +- Accuracy: Missing, conflicting, or stale rates create findings. A report + can only claim that provided records matched. A fund total receipt does not + prove individual worker credit, and a local calculation is not legal advice. +- Scale: replace SafeAgent Control's known factorial matching path before + large datasets. Demonstrate period-sized input without silently dropped + rows or incorrect ambiguity resolution. +- Security: use synthetic fixture data in GitHub; process real worker records + only with operator authorization in a controlled local environment. +- Commercial: seek one paid continuation after a reproducible discrepancy or + documented recurring time saving. Publishing packages, gaining installs, + receiving technical reviews, and gaining social followers do not count. + +## Existing assets to reuse when justified + +SafeAgent Control's independent matching and source adapters; the Python +package for local processing; claim/settle for a later human sign-off gate; +n8n for scheduled ingestion; evidence bundles and the dashboard for review. +The n8n v0.2.5 node is limited to ten test calls per IP and is not a production +path. x402 pay-per-call is not an enterprise contract mechanism. Preserve +cryptographic source digests without publishing payroll data. The Stripe +EC-009 test is a payment boundary experiment, not customer validation. + +## Starting buyer and incumbent sources + +- [DOL WH-347 instructions](https://www.dol.gov/agencies/whd/forms/wh347) +- [SMACNA Miami Valley chapter directory](https://portal.smacna.org/eweb/DynamicPage.aspx?webcode=ChapterDirectory) +- [CFMA chapters](https://beta.cfma.org/chapters) +- [IFEBP collection procedures](https://www.ifebp.org/education---events/educational-program-schedule/collection-procedures-institute) +- [LaborAid contractor product](https://laboraid.com/contractor) +- [LCPcertified product](https://lcptracker.com/lcpcertified/) +- [ThirdLine named contingency audit](https://www.thirdline.io/case-studies/virginia-beach-contingency-audit) diff --git a/products/fringe_proof/.gitignore b/products/fringe_proof/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/products/fringe_proof/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/products/fringe_proof/README.md b/products/fringe_proof/README.md new file mode 100644 index 0000000..9d02bc5 --- /dev/null +++ b/products/fringe_proof/README.md @@ -0,0 +1,62 @@ +# SafeAgent Control: Fringe Proof (pilot slice) + +A local, read-only check of **provided** payroll hours, effective-dated fund +rates, worker-level remittance lines, and aggregate fund receipts. It never +initiates payroll or payment. No employer or worker data is sent to SafeAgent. + +The included synthetic example demonstrates a $8.00 discrepancy: a fund +acknowledges exactly the submitted $160.00, while the approved rate schedule +and covered hours imply $168.00 for that worker after a mid-period rate change. +An acknowledgment for an aggregate fund amount cannot establish worker-level +credit or a legally compliant contribution. + +Run with Python 3.10+ and no third-party dependencies: + +```bash +cd products/fringe_proof +python reconcile.py \ + --payroll fixtures/payroll.csv --rates fixtures/rates.csv \ + --remittance fixtures/remittance.csv --fund-ack fixtures/fund_ack.csv \ + --period-start 2026-09-01 --period-end 2026-09-07 \ + --output /tmp/fringe-proof-report.json +``` + +Exit code 2 means the report contains findings. Exit code 0 means the +**provided records** matched within this narrow comparison; it does not +certify wage compliance, fund credit, or completeness of the input exports. +Input SHA-256 digests and a canonical report digest are saved in the JSON. +Keep real input files and reports in an access-controlled local location; +never commit them to GitHub. + +## Required columns + +| CSV | Columns | +| --- | --- | +| payroll | `employee_id,work_date,local,classification,covered_hours` | +| rates | `local,classification,fund,effective_from,effective_to,rate_per_hour` | +| remittance | `employee_id,local,classification,fund,reported_hours,reported_amount` | +| fund_ack | `local,fund,amount_received` | + +Rates must be supplied and approved by the operator. `effective_to` may be +blank. One unambiguous rate for each fund and work date is required. Remittance +amounts/hours may be negative for corrections; this pilot aggregates them by +worker, local, classification and fund. Fund receipts are compared by local +and fund for this period; if the fund issues separate batches, aggregate them +under the operator's documented process before using this CSV. The pilot uses +half-up rounding at the worker/fund period total. An operator must confirm +the applicable fund agreement uses that rule before interpreting amounts. + +The product question is whether independent cross-source checks catch a +previously unknown, actionable discrepancy, or remove meaningful manual +review time. Current competitors already validate payroll reports and automate +remittances. This code is a test of a narrower gap, not market validation. + +## Next engineering gate + +SafeAgent Control's existing matching prototype has a reported factorial +slowdown above nine claims. Replace that matching path and establish a +representative large-period benchmark before adapting it for a real employer. +Do not add automatic certification or fund payments without a verified source +of rules and an explicit human review workflow. The existing SafeAgent +claim/settle layer can then gate a sign-off; n8n, Python, MCP, dashboard, +and evidence receipts can import, display, or preserve the reviewed result. diff --git a/products/fringe_proof/fixtures/fund_ack.csv b/products/fringe_proof/fixtures/fund_ack.csv new file mode 100644 index 0000000..d2729c4 --- /dev/null +++ b/products/fringe_proof/fixtures/fund_ack.csv @@ -0,0 +1,2 @@ +local,fund,amount_received +LOCAL-24,HEALTH,190.00 diff --git a/products/fringe_proof/fixtures/payroll.csv b/products/fringe_proof/fixtures/payroll.csv new file mode 100644 index 0000000..2fe7a16 --- /dev/null +++ b/products/fringe_proof/fixtures/payroll.csv @@ -0,0 +1,4 @@ +employee_id,work_date,local,classification,covered_hours +WORKER-001,2026-09-01,LOCAL-24,journeyworker,8 +WORKER-001,2026-09-02,LOCAL-24,journeyworker,8 +WORKER-002,2026-09-02,LOCAL-24,apprentice,8 diff --git a/products/fringe_proof/fixtures/rates.csv b/products/fringe_proof/fixtures/rates.csv new file mode 100644 index 0000000..cff11cb --- /dev/null +++ b/products/fringe_proof/fixtures/rates.csv @@ -0,0 +1,4 @@ +local,classification,fund,effective_from,effective_to,rate_per_hour +LOCAL-24,journeyworker,HEALTH,2026-01-01,2026-09-01,10.00 +LOCAL-24,journeyworker,HEALTH,2026-09-02,,11.00 +LOCAL-24,apprentice,HEALTH,2026-01-01,,5.00 diff --git a/products/fringe_proof/fixtures/remittance.csv b/products/fringe_proof/fixtures/remittance.csv new file mode 100644 index 0000000..4347c7e --- /dev/null +++ b/products/fringe_proof/fixtures/remittance.csv @@ -0,0 +1,3 @@ +employee_id,local,classification,fund,reported_hours,reported_amount +WORKER-001,LOCAL-24,journeyworker,HEALTH,16,160.00 +WORKER-002,LOCAL-24,apprentice,HEALTH,6,30.00 diff --git a/products/fringe_proof/reconcile.py b/products/fringe_proof/reconcile.py new file mode 100644 index 0000000..c3be1e9 --- /dev/null +++ b/products/fringe_proof/reconcile.py @@ -0,0 +1,195 @@ +"""Local, read-only comparison of payroll, union remittance, and fund receipts. + +This is an evidence tool, not a payroll calculator or a compliance certification. +Only customer-approved rate tables are used; missing or ambiguous rules are findings. +""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +from collections import defaultdict +from datetime import date +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP +from pathlib import Path + +CENT = Decimal("0.01") +FIELDS = { + "payroll": ("employee_id", "work_date", "local", "classification", "covered_hours"), + "rates": ("local", "classification", "fund", "effective_from", "effective_to", "rate_per_hour"), + "remittance": ("employee_id", "local", "classification", "fund", "reported_hours", "reported_amount"), + "fund_ack": ("local", "fund", "amount_received"), +} + + +def day(value: str) -> date: + try: + return date.fromisoformat(value) + except ValueError as exc: + raise ValueError(f"invalid ISO date: {value!r}") from exc + + +def number(value: str, label: str, *, signed: bool = False) -> Decimal: + try: + result = Decimal(value) + except InvalidOperation as exc: + raise ValueError(f"invalid {label}: {value!r}") from exc + if not result.is_finite() or (not signed and result < 0): + raise ValueError(f"invalid {label}: {value!r}") + return result + + +def money(value: Decimal) -> str: + return str(value.quantize(CENT, rounding=ROUND_HALF_UP)) + + +def load(path: Path, kind: str) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8-sig") as handle: + reader = csv.DictReader(handle) + if not reader.fieldnames or len(reader.fieldnames) != len(set(reader.fieldnames)): + raise ValueError(f"{path}: missing or duplicate CSV headers") + missing = set(FIELDS[kind]) - set(reader.fieldnames) + if missing: + raise ValueError(f"{path}: missing columns {sorted(missing)}") + rows = [] + for line, row in enumerate(reader, start=2): + if None in row or any(not row[field].strip() for field in FIELDS[kind] + if field != "effective_to"): + raise ValueError(f"{path}:{line}: missing value or extra column") + rows.append({key: value.strip() for key, value in row.items()}) + return rows + + +def compare(payroll: list[dict[str, str]], rates: list[dict[str, str]], + remittance: list[dict[str, str]], acknowledgments: list[dict[str, str]], + start: date, end: date) -> list[dict[str, str]]: + if start > end: + raise ValueError("period start is after end") + findings: list[dict[str, str]] = [] + for kind, rows in (("payroll", payroll), ("rates", rates), + ("remittance", remittance), ("fund_ack", acknowledgments)): + if not rows: + findings.append({"type": "EMPTY_SOURCE", "source": kind}) + rules: dict[tuple[str, str, str], list[tuple[date, date | None, Decimal]]] = defaultdict(list) + for row in rates: + begin = day(row["effective_from"]) + finish = day(row["effective_to"]) if row["effective_to"] else None + if finish is not None and finish < begin: + raise ValueError("rate effective_to precedes effective_from") + key = row["local"], row["classification"], row["fund"] + rules[key].append((begin, finish, number(row["rate_per_hour"], "rate"))) + + # Keep exact decimal values until the employee/fund period total is calculated. + expected: dict[tuple[str, str, str, str], list[Decimal]] = defaultdict(lambda: [Decimal(0), Decimal(0)]) + for row in payroll: + work_date = day(row["work_date"]) + if not start <= work_date <= end: + raise ValueError(f"payroll work_date {work_date} outside selected period") + hours = number(row["covered_hours"], "covered_hours") + base = row["employee_id"], row["local"], row["classification"] + candidate_funds = sorted({fund for local, classification, fund in rules + if (local, classification) == base[1:]}) + if not candidate_funds: + findings.append({"type": "NO_RATE", "employee_id": base[0], + "local": base[1], "classification": base[2], "work_date": str(work_date)}) + for fund in candidate_funds: + active = [rate for begin, finish, rate in rules[base[1], base[2], fund] + if begin <= work_date and (finish is None or work_date <= finish)] + if len(active) != 1: + findings.append({"type": "AMBIGUOUS_RATE" if active else "NO_RATE", + "employee_id": base[0], "local": base[1], + "classification": base[2], "fund": fund, "work_date": str(work_date)}) + continue + totals = expected[(*base, fund)] + totals[0] += hours + totals[1] += hours * active[0] + + reported: dict[tuple[str, str, str, str], list[Decimal]] = defaultdict(lambda: [Decimal(0), Decimal(0)]) + for row in remittance: + key = tuple(row[field] for field in ("employee_id", "local", "classification", "fund")) + reported[key][0] += number(row["reported_hours"], "reported_hours", signed=True) + reported[key][1] += number(row["reported_amount"], "reported_amount", signed=True) + for key in sorted(expected.keys() | reported.keys()): + labels = dict(zip(("employee_id", "local", "classification", "fund"), key)) + if key not in expected: + findings.append({"type": "UNMATCHED_REMITTANCE", **labels}) + elif key not in reported: + findings.append({"type": "MISSING_REMITTANCE", **labels, + "expected_hours": str(expected[key][0]), + "expected_amount": money(expected[key][1])}) + else: + if expected[key][0] != reported[key][0]: + findings.append({"type": "HOURS_DELTA", **labels, + "payroll_hours": str(expected[key][0]), + "reported_hours": str(reported[key][0])}) + if money(expected[key][1]) != money(reported[key][1]): + findings.append({"type": "AMOUNT_DELTA", **labels, + "expected_amount": money(expected[key][1]), + "reported_amount": money(reported[key][1])}) + + # A fund-level receipt proves an aggregate amount only, never a worker allocation. + received: dict[tuple[str, str], Decimal] = {} + for row in acknowledgments: + key = row["local"], row["fund"] + if key in received: + findings.append({"type": "DUPLICATE_FUND_ACK", "local": key[0], "fund": key[1]}) + continue + received[key] = number(row["amount_received"], "amount_received") + submitted: dict[tuple[str, str], Decimal] = defaultdict(Decimal) + for (_, local, _, fund), (_, amount) in reported.items(): + submitted[local, fund] += amount + for key in sorted(submitted.keys() | received.keys()): + labels = {"local": key[0], "fund": key[1]} + if key not in submitted: + findings.append({"type": "UNMATCHED_FUND_ACK", **labels}) + elif key not in received: + findings.append({"type": "MISSING_FUND_ACK", **labels}) + elif money(submitted[key]) != money(received[key]): + findings.append({"type": "FUND_AMOUNT_DELTA", **labels, + "submitted_amount": money(submitted[key]), + "received_amount": money(received[key])}) + return sorted(findings, key=lambda finding: json.dumps(finding, sort_keys=True)) + + +def run(paths: dict[str, Path], start: date, end: date) -> dict: + sources = {kind: load(path, kind) for kind, path in paths.items()} + findings = compare(sources["payroll"], sources["rates"], sources["remittance"], + sources["fund_ack"], start, end) + report = { + "product": "SafeAgent Control: Fringe Proof", + "scope": "comparison of provided records only; not a compliance certification", + "period": {"start": str(start), "end": str(end)}, + "input_sha256": {kind: hashlib.sha256(path.read_bytes()).hexdigest() + for kind, path in sorted(paths.items())}, + "input_rows": {kind: len(rows) for kind, rows in sorted(sources.items())}, + "status": "EXCEPTIONS" if findings else "MATCHED_TO_PROVIDED_RECORDS", + "findings": findings, + } + canonical = json.dumps(report, sort_keys=True, separators=(",", ":")).encode() + report["report_sha256"] = hashlib.sha256(canonical).hexdigest() + return report + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + for kind in FIELDS: + parser.add_argument("--" + kind.replace("_", "-"), type=Path, required=True) + parser.add_argument("--period-start", type=day, required=True) + parser.add_argument("--period-end", type=day, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + paths = {kind: getattr(args, kind) for kind in FIELDS} + try: + result = run(paths, args.period_start, args.period_end) + except (ValueError, OSError) as exc: + parser.error(str(exc)) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps({"status": result["status"], "findings": len(result["findings"]), + "report": str(args.output)})) + return 0 if not result["findings"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/products/fringe_proof/test_reconcile.py b/products/fringe_proof/test_reconcile.py new file mode 100644 index 0000000..39c13b8 --- /dev/null +++ b/products/fringe_proof/test_reconcile.py @@ -0,0 +1,79 @@ +"""Behavioral checks for source discrepancies and refusal to infer unknown rates.""" + +import csv +import tempfile +import unittest +from datetime import date +from pathlib import Path + +from reconcile import run + + +SCHEMAS = { + "payroll": ("employee_id", "work_date", "local", "classification", "covered_hours"), + "rates": ("local", "classification", "fund", "effective_from", "effective_to", "rate_per_hour"), + "remittance": ("employee_id", "local", "classification", "fund", "reported_hours", "reported_amount"), + "fund_ack": ("local", "fund", "amount_received"), +} + + +class FringeProofTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.paths = {name: Path(self.temp.name) / (name + ".csv") for name in SCHEMAS} + self.rows = { + "payroll": [ + ("WORKER-001", "2026-09-01", "L24", "journeyworker", "8"), + ("WORKER-001", "2026-09-02", "L24", "journeyworker", "8"), + ], + "rates": [ + ("L24", "journeyworker", "HEALTH", "2026-01-01", "2026-09-01", "10"), + ("L24", "journeyworker", "HEALTH", "2026-09-02", "", "11"), + ], + "remittance": [("WORKER-001", "L24", "journeyworker", "HEALTH", "16", "160.00")], + "fund_ack": [("L24", "HEALTH", "160.00")], + } + + def report(self): + for name, rows in self.rows.items(): + with self.paths[name].open("w", newline="") as handle: + writer = csv.writer(handle) + writer.writerow(SCHEMAS[name]) + writer.writerows(rows) + return run(self.paths, date(2026, 9, 1), date(2026, 9, 7)) + + def test_fund_receipt_can_match_an_incorrect_worker_amount(self): + report = self.report() + self.assertEqual(report["status"], "EXCEPTIONS") + amount = [finding for finding in report["findings"] if finding["type"] == "AMOUNT_DELTA"] + self.assertEqual(len(amount), 1) + self.assertEqual(amount[0]["expected_amount"], "168.00") + self.assertEqual(amount[0]["reported_amount"], "160.00") + self.assertFalse(any(f["type"] == "FUND_AMOUNT_DELTA" for f in report["findings"])) + self.assertEqual(report, self.report()) + + def test_missing_fund_receipt_is_unverified_not_a_match(self): + self.rows["remittance"] = [("WORKER-001", "L24", "journeyworker", "HEALTH", "16", "168.00")] + self.rows["fund_ack"] = [] + findings = self.report()["findings"] + self.assertEqual({f["type"] for f in findings}, {"EMPTY_SOURCE", "MISSING_FUND_ACK"}) + + def test_rate_overlap_requires_human_resolution(self): + self.rows["rates"].append(("L24", "journeyworker", "HEALTH", "2026-09-02", "", "12")) + self.assertIn("AMBIGUOUS_RATE", {f["type"] for f in self.report()["findings"]}) + + def test_out_of_period_records_are_rejected(self): + self.rows["payroll"].append(("WORKER-002", "2026-08-31", "L24", "journeyworker", "8")) + with self.assertRaisesRegex(ValueError, "outside selected period"): + self.report() + + def test_all_empty_files_cannot_return_a_clean_report(self): + self.rows = {name: [] for name in SCHEMAS} + report = self.report() + self.assertEqual(report["status"], "EXCEPTIONS") + self.assertEqual(len(report["findings"]), 4) + + +if __name__ == "__main__": + unittest.main() From 4f5b8015c8b62e7a9d419de7aada62d35f4f73e1 Mon Sep 17 00:00:00 2001 From: azender1 Date: Thu, 24 Sep 2026 22:09:43 -0400 Subject: [PATCH 2/2] Record named first-hand contractor leads with explicit demand gaps --- products/fringe_proof/BUYER_EVIDENCE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 products/fringe_proof/BUYER_EVIDENCE.md diff --git a/products/fringe_proof/BUYER_EVIDENCE.md b/products/fringe_proof/BUYER_EVIDENCE.md new file mode 100644 index 0000000..acd684a --- /dev/null +++ b/products/fringe_proof/BUYER_EVIDENCE.md @@ -0,0 +1,22 @@ +# Firsthand buyer signals (research leads, not customers) + +As of 2026-09-25, these people publicly described actual operational work. +None has requested SafeAgent, supplied data, or agreed to pay. Verify that +the issue is still open and that existing software does not already fix it. +Do not send outreach from an unattended agent. + +| Priority | Operator's public statement | Pilot hypothesis and important counterpoint | +| --- | --- | --- | +| 1 | [Thomas Cornellier, CEO of TSI/Exterior Wall Systems](https://cafe.cfma.org/discussion/payroll-software-for-union-contractors-1?hlmlt=VT), wrote on 2025-10-15 that the company runs union payroll in house and existing solutions leave manual work. | Ask whether comparing time, payroll, remittance and fund receipt for one period would uncover a discrepancy. The thread recommended Foundation, Sage, and Miter; determine whether a purchase since resolved the problem. | +| 2 | [Shawn Erickson, CFO of C.J. Erickson Plumbing](https://cafe.cfma.org/discussion/updating-employee-wagesfringes-in-lcp-tracker-1?hlmlt=VT), asked on 2025-06-10 whether a new union rate requires updating each worker's LCPtracker record individually. A reply described weekly uploads plus manual fringe record changes. | Offer a pre-submission rate-change check across payroll and reported fringe for a pay period. The question is over a year old and may already be solved; do not assume current demand. | +| 3 | [Nicholas Siano, controller of Northstar Refrigeration](https://cafe.cfma.org/discussion/miter-for-union-shop-payroll-buildopssage-intacct), described on 2025-11-11 an existing BuildOps → Sage Intacct setup and evaluation of Miter for complex union and certified payroll. | Ask about records that diverge across the three tools before importing any data. Forum replies in 2026 report Miter works well elsewhere; existing software may remove this gap. | + +Other firsthand signal: an [anonymous construction owner](https://www.reddit.com/r/Payroll/comments/1ra0mhd/need_help_fixing_davisbacon_certified_payroll/) +described subcontractor sign-in sheets disagreeing with certified payroll, +and was weighing an employee versus an outsourced service. Identity, budget, +and authenticity have not been verified; do not count as a named lead. + +Price and incumbent context: [LaborAid publishes contractor pricing](https://laboraid.com/contractor), +and [LCPcertified](https://lcptracker.com/lcpcertified/) already validates +reports. Published pricing and public forum questions show category spending +and workarounds, not SafeAgent product-market fit.