Skip to article

US federal award data · Reproducible Python workflow

US federal contract awards analyzed safely with Python.

Verify and query a pinned US federal contract awards release with Python and DuckDB, join awards to suppliers, and keep cumulative obligations, values, and change-window semantics distinct.

Published August 27, 202625 min readBy DanielReviewed by Alexandra

Analyze a US federal contract-awards release as a changed-record snapshot: pin the edition, verify every input, keep one row per award_id, and label cumulative amounts for what they are. The edition date is not the award date, change_type=new is not proof of a newly signed contract, and a supplier roll-up is not another pool of dollars to add to the awards table.

This worked example uses WebTruffle's first US federal contract awards release, tagged August 25, 2026. It contains 17,945 prime-award summaries, 3,145 supplier identities, and 17,945 award-to-prime-supplier relationships selected through an inclusive August 23–25 USAspending last_modified_date window.

The most important empirical check is also the easiest one to miss: 16,825 of the 17,945 awards—93.76%—have a base_action_date before that three-day window. All 17,945 rows are marked new because this is the pipeline's first comparison baseline. They are not 17,945 contracts signed in three days.

The companion Python recipe pins the exact release, verifies the manifest and all three CSV products, validates 66 award fields, 13 supplier fields, 10 relationship fields, and every join key, then queries verified local files with DuckDB 1.5.5. Its outputs say changed-window wherever an unlabeled “spend” or “market share” would be misleading.

Pinned release receiptVerify the edition before Python parses a CSV.
Initial baseline17,945 rows marked newNew to this pipeline, not necessarily newly signed.
Release tag
2026-08-25
Source window
Aug 23–25, 2026
Date filter
last_modified_date
Base awards before window
16,825
Products, record counts, declared grains, and analytical roles in release 2026-08-25
ProductRowsDeclared grainRole
us-federal-contract-awards17,945One current award summary per award_idAmount-bearing grain
suppliers3,145One normalized prime supplier in the editionIdentity lookup
award-suppliers17,945One award-to-prime-supplier relationshipJoin bridge

Receipt rule: pin the tag, read the manifest, verify byte counts and SHA-256, then assert the product grain and row count before analysis.

US federal contract awards with Python: the short answer

Use this sequence:

  1. Pin a tagged release and an independently recorded manifest SHA-256. A moving latest URL is convenient for discovery, not reproducibility.
  2. Verify the manifest's exact bytes and digest before trusting the file hashes declared inside it.
  3. Download and verify schema.json, us-federal-contract-awards.csv, suppliers.csv, and award-suppliers.csv locally.
  4. Validate the complete ordered headers from the verified schema: 66, 13, and 10 fields respectively.
  5. Require one unique award_id per award row, one unique supplier_id per supplier row, and one relationship per award in this edition.
  6. Treat the release as an award-summary change window selected by last_modified_date, not an action ledger selected by award date.
  7. Keep identifiers and source-formatted decimal amounts as strings at ingestion. Cast only the columns needed by one query.
  8. Use award_id for award-grain joins and supplier_id for the normalized recipient join. Keep piid and recipient_name as references and labels.
  9. Aggregate monetary fields once from the awards table. Never add supplier totals to award totals or sum repeated snapshots across editions.
  10. Measure field presence before filtering or ranking. A missing outlay is unknown, not zero.
  11. Label every grouping as the composition of awards changed in this edition. It is not total procurement demand, agency market share, or daily spending.
  12. Export bounded results with the release tag, window, hashes, script version, query name, row count, and output digest.

If you only need the current files and field definitions, start with the free US federal contract awards dataset. Use this workflow when an analysis, review queue, or handoff must be repeatable.

Pin the 2026-08-25 release before querying

A release address and a file identity are different things. GitHub release assets can be replaced; the independently recorded hash is what detects changed bytes. Pin the manifest first, then accept data-file declarations only from that verified manifest.

The worked evidence package is:

release tag          2026-08-25
coverage window      2026-08-23 through 2026-08-25 inclusive
coverage date type   last_modified_date
generated at         2026-08-26T14:01:08Z
schema version       1.0
manifest bytes       11,153
manifest SHA-256     fcfe470fc3ef678319f88471849f8f4d448ceaafa406bd40584b876216443c7e

schema bytes         19,271
schema SHA-256       12d2718f6f057d6a64dc09574652cfdb17625f7ab8434d6424f58013db73f1f6

awards CSV bytes     20,403,764
awards CSV SHA-256   bcf2f1131dcd7de8df89534dae3d5f4ef8c700418bd7b60d23a81aeb5e6a69fa
suppliers CSV bytes  548,038
suppliers SHA-256    f9bd32daf4d8a909c70319c7d650f57bf137728580e9fc2c95b30dfc46dc695a
links CSV bytes      4,026,066
links CSV SHA-256    205cb9a1ed68512ff8633c17f77cd8b2b4a240feb27b06febaad89c60d21ee3f

The pinned manifest declares the product grains, record counts, hashes, the awards product's 66-field order, source reconciliation, date and amount semantics, and known governance assets. The verified schema.json supplies the canonical definitions and ordered headers for all three products. The manifest reports 18,337 source records downloaded, zero duplicate supersessions, 392 records excluded by the April 4, 2022 base-action cutoff, zero excluded for a missing base date, and 17,945 published awards.

That count equation is a release-health checkpoint:

18,337 downloaded
−     0 duplicate rows superseded
−   392 pre-cutoff awards
−     0 awards missing a provable base date
=17,945 published award summaries

A matching equation and hash establish which artifact you analyzed. They do not prove that the upstream source is complete or that an interpretation is correct.

Install the tested Python environment

The recipe requires Python 3.10 or newer and pins DuckDB 1.5.5. DuckDB's official Python client can read local CSVs with all columns as text, execute SQL, and copy bounded results directly to CSV. (DuckDB Python API; CSV reader)

On macOS, Linux, or WSL:

curl -fsSLO https://www.webtruffle.com/examples/us-federal-contract-awards-python.py
curl -fsSLO https://www.webtruffle.com/examples/us-federal-contract-awards-python-requirements.txt

python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r us-federal-contract-awards-python-requirements.txt
python us-federal-contract-awards-python.py

On Windows PowerShell:

Invoke-WebRequest `
  -Uri https://www.webtruffle.com/examples/us-federal-contract-awards-python.py `
  -OutFile us-federal-contract-awards-python.py
Invoke-WebRequest `
  -Uri https://www.webtruffle.com/examples/us-federal-contract-awards-python-requirements.txt `
  -OutFile us-federal-contract-awards-python-requirements.txt

py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r us-federal-contract-awards-python-requirements.txt
python .\us-federal-contract-awards-python.py

The default directory is us-federal-contract-awards-2026-08-25. Put the evidence beside a project when the result must survive review:

python us-federal-contract-awards-python.py \
  --data-dir ./evidence/us-federal-awards-2026-08-25

An existing file with the wrong size or hash causes a hard failure. The recipe never silently overwrites it. Missing inputs download to unique temporary files in the destination directory, stop if they exceed the declared byte count, and move into place only after both size and SHA-256 match.

Verify the manifest, bytes, and SHA-256

Verification begins outside DuckDB so the analytical engine never decides which bytes count as evidence:

import hashlib
from pathlib import Path

def sha256_and_size(path: Path) -> tuple[str, int]:
    digest = hashlib.sha256()
    size = 0
    with path.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)
            size += len(chunk)
    return digest.hexdigest(), size

manifest_path = Path("manifest.json")
actual_hash, actual_bytes = sha256_and_size(manifest_path)
assert actual_bytes == 11_153
assert actual_hash == (
    "fcfe470fc3ef678319f88471849f8f4d"
    "448ceaafa406bd40584b876216443c7e"
)

Only then load the declarations inside it:

import json

manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert manifest["dataset_id"] == "us-federal-contract-awards"
assert manifest["target_date"] == "2026-08-25"
assert manifest["record_count"] == 17_945
assert manifest["supplier_count"] == 3_145
assert manifest["relationship_count"] == 17_945

awards = manifest["files"]["us-federal-contract-awards.csv"]
assert awards["bytes"] == 20_403_764
assert awards["sha256"] == (
    "bcf2f1131dcd7de8df89534dae3d5f4e"
    "f8c700418bd7b60d23a81aeb5e6a69fa"
)

Read CSV headers with encoding="utf-8-sig". The release files begin with a UTF-8 byte-order mark; plain utf-8 can turn the first header into \ufeffedition_date in a standard-library parser.

import csv

with open(
    "us-federal-contract-awards.csv",
    encoding="utf-8-sig",
    newline="",
) as source:
    headers = next(csv.reader(source))

assert headers == manifest["record_fields"]
assert len(headers) == 66
assert len(headers) == len(set(headers))

The hash proves byte identity, not field accuracy, source completeness, or analytical fitness. Those remain separate tests.

Treat the edition as a change window, not an award date

The collector requested prime-award summaries whose upstream last_modified_date fell from August 23 through August 25, inclusive. It did not request awards whose base_action_date fell in that interval.

Four clocks · one changed-summary editionAn edition date is not an award date.
Edition date
01
2026-08-25
Logical release date and inclusive end of the source overlap window.
Source selection
02
Aug 23–25
Awards whose source summary matched last_modified_date in the three-day window.
Base action
03
Original award
The government action establishing the award; it can be years older than the edition.
Latest action
04
Latest represented change
The most recent contract-action date rolled into the current award summary.

Most baseline awards have a base action before the source window.

Membership in this edition says the award summary was modified during the window. It does not say the base action occurred then.

16,825 of 17,945 base awards pre-window
Base action before Aug 23 · 16,825Base action in window · 1,120

Baseline status

All 17,945 rows are new to the pipeline.

That status records first observation, not a newly signed federal contract.

Fiscal-year boundary

Summary balances do not measure FY obligation flow.

A fiscal-year total requires transaction obligations grouped by each action date.

The six useful clocks answer different questions:

  • edition_date names the release and ends the overlap window.
  • last_modified_at is the upstream award-summary modification timestamp used for collection.
  • base_action_date is the original award action represented by the summary.
  • latest_action_date is the latest contract action represented by that summary.
  • performance dates describe the reported performance period.
  • first_seen_at and last_seen_at are WebTruffle observation timestamps.

This release makes the difference measurable:

SELECT
  count(*) AS award_rows,
  count(*) FILTER (
    WHERE try_cast(base_action_date AS DATE)
          < DATE '2026-08-23'
  ) AS base_award_before_window,
  count(*) FILTER (
    WHERE change_type = 'new'
  ) AS baseline_new_rows
FROM awards;
award_rows               17,945
base_award_before_window 16,825
baseline_new_rows        17,945

new means first seen by this pipeline. On a later edition, it can identify a first pipeline observation inside that edition; it still does not replace base_action_date or prove when an award was signed.

USAspending distinguishes award summaries from the underlying transactions that establish or modify them. A summary generally reflects the current rolled-up state; a fiscal-year obligations flow requires transaction-level action obligations grouped by action date. (USAspending transaction endpoint; Federal Spending Guide)

Choose awards, suppliers, or award-supplier relationships

The release has three analytical products, not three interchangeable copies of the same table.

Three products · three grainsRelationships connect identities; they do not create another copy of award value.
Amounts stay on award grain
  1. Product 0117,945 rows

    Awards

    Key: award_id

    One current prime contract award summary in this edition

    Obligated, outlay, current-value, and potential-value fields live here.

  2. Product 0217,945 rows

    Award suppliers

    Key: award_id + supplier_id

    One explicit award-to-current-prime-supplier relationship

    No monetary fields. This bridge identifies the prime recipient, not a subcontractor.

  3. Product 033,145 rows

    Suppliers

    Key: supplier_id

    One normalized prime supplier identity within the edition

    Edition award counts and obligation totals are derived from the award rows present here.

Not in award rows
Individual transaction or modification records
Not in v1
IDV master awards and parent/order relationships
Not a supplier link
Subawards or subcontractors
Not an event count
One row per contract action

Aggregate monetary fields from us-federal-contract-awards only after selecting one desired snapshot per award_id. Use the relationship and supplier products to attach identity—not to multiply amounts.

  • us-federal-contract-awards.csv has one current prime-award summary per award_id in the edition. Count awards and aggregate award-level amounts here.
  • suppliers.csv has one normalized prime-recipient identity represented in this edition. Its award_count and obligation fields summarize only awards present in this change window.
  • award-suppliers.csv has one award-to-current-prime-supplier relationship in this edition. It carries join keys and labels, not subcontractor or corporate-family relationships.

The relationship product has 17,945 rows because every inaugural award has one current prime recipient. That is an observed property of this release, not a promise that every future relationship model will always remain one-to-one.

The release excludes individual contract actions, parent IDV products, subawards, opportunities, dedicated contact fields, and DUNS or D&B-derived legacy fields. Free-text descriptions can still contain names or identifying material. Read the pinned schema documentation before turning an absent object into a negative conclusion.

Validate schema, keys, and referential integrity

Validate each table before joining it:

SELECT count(*) AS rows, count(DISTINCT award_id) AS keys
FROM awards;

SELECT count(*) AS rows, count(DISTINCT supplier_id) AS keys
FROM suppliers;

SELECT
  count(*) AS rows,
  count(DISTINCT award_id) AS award_keys,
  count(DISTINCT supplier_id) AS supplier_keys
FROM award_suppliers;

The expected result is 17,945 unique award keys, 3,145 unique supplier keys, and 17,945 relationship rows referencing those same populations.

Then prove there are no orphans or disagreements:

SELECT count(*) AS orphan_awards
FROM award_suppliers AS rel
LEFT JOIN awards AS award
  ON award.award_id = rel.award_id
WHERE award.award_id IS NULL;

SELECT count(*) AS orphan_suppliers
FROM award_suppliers AS rel
LEFT JOIN suppliers AS supplier
  ON supplier.supplier_id = rel.supplier_id
WHERE supplier.supplier_id IS NULL;

SELECT count(*) AS supplier_key_disagreements
FROM award_suppliers AS rel
JOIN awards AS award
  ON award.award_id = rel.award_id
WHERE award.supplier_id IS DISTINCT FROM rel.supplier_id;

All three counts are zero in the pinned edition.

Use award_id as the award key. piid remains useful for a human-facing procurement reference, but it is not guaranteed to be globally sufficient by itself. GSA's Contract Awards API documents cases where PIID-family aggregation also requires referenced-IDV context. (SAM.gov Contract Awards API)

Use supplier_id for the normalized recipient join. The identity policy prefers UEI, then CAGE, then an award-scoped unresolved key. recipient_name is a source-published label, not an identity key, and no fuzzy corporate-family matching is performed. Every row in this inaugural edition has a UEI, so the documented fallback paths are not exercised by this release.

Load the three CSV products with DuckDB

Download first, verify second, materialize third:

from pathlib import Path
import duckdb

data_dir = Path("us-federal-contract-awards-2026-08-25")
connection = duckdb.connect()

for view, filename in {
    "awards": "us-federal-contract-awards.csv",
    "suppliers": "suppliers.csv",
    "award_suppliers": "award-suppliers.csv",
}.items():
    relation = connection.read_csv(
        str(data_dir / filename),
        header=True,
        all_varchar=True,
    )
    relation.create_view(f"{view}_source")
    connection.execute(
        f"CREATE TEMP TABLE {view} AS "
        f"SELECT * FROM {view}_source"
    )
    connection.execute(f"DROP VIEW {view}_source")

all_varchar=True is deliberate. It prevents a sample-driven type inference from turning an identifier into a number, flattening an offset timestamp, or rounding a source decimal through a binary float. Query-specific TRY_CAST calls create a typed analytical lane while the original string remains intact.

Materialization also closes a lazy-evaluation gap. The recipe verifies local files, copies them into temporary DuckDB tables, rechecks the input hashes, then exports from those fixed in-memory relations. Later on-disk changes cannot affect those materialized tables, and the second hash check detects an ordinary file change before export.

Keep obligations, outlays, current value, and potential value separate

The four award-level monetary fields are not synonyms.

Four balances · four questionsUSD does not make the amount fields interchangeable.
Award-grain measures
Amount fields, their meanings, and interpretation boundaries
FieldMeasureMeaningBoundary
total_obligated_amount_usdCommitmentCumulative amount legally committed on the award at this snapshot.Not a cash payment
total_outlay_amount_usdDisbursementCumulative cash paid against the award when the source reports it.Present on 1,580 baseline rows
current_total_value_of_award_usdCurrent valueCurrent procurement value including the base and exercised options.Distinct from obligations
potential_total_value_of_award_usdPotential valuePossible value if all available options are exercised.Not promised spend
Serialization
Read monetary values as decimal strings and cast with decimal-safe logic, not binary floating point.
Missingness
Keep null distinct from a reported zero. Do not fill missing outlays or values with zero.
Across releases
Select one snapshot per award_id before aggregating cumulative balances.

Do not infer supplier revenue, cash received, backlog, or “remaining spend” by substituting or subtracting these measures. The relationship file has no amount column by design.

  • total_obligated_amount_usd is the award's cumulative legal commitments at the snapshot. It is not cash paid or dollars obligated during the edition.
  • total_outlay_amount_usd is cumulative cash disbursed when reported. Blank is missing; it must not be coerced to zero.
  • current_total_value_of_award_usd is current procurement value including the base and exercised options.
  • potential_total_value_of_award_usd includes possible option value. It is not promised spending or supplier backlog.

The CSV stores canonical decimal strings. Preserve zero, null, sign, and precision. Use a fixed decimal type only after checking the range needed by your query:

SELECT
  count(*) AS award_rows,
  count(total_obligated_amount_usd) FILTER (
    WHERE total_obligated_amount_usd <> ''
  ) AS obligation_present,
  count(total_outlay_amount_usd) FILTER (
    WHERE total_outlay_amount_usd <> ''
  ) AS outlay_present,
  sum(try_cast(total_obligated_amount_usd AS DECIMAL(38, 2)))
    AS cumulative_obligations_on_changed_awards
FROM awards;

The pinned edition has obligations on all 17,945 award rows and outlays on only 1,580—8.80%. The obligation sum can be labeled “cumulative obligations on awards represented in this change window.” Calling it “federal spend from August 23–25” would be false.

Do not calculate “remaining spend” by subtracting obligations from potential value. Do not call obligations supplier revenue, and do not call outlays current-period cash receipts. The fields describe different cumulative federal measures at the source snapshot.

Join awards and suppliers without duplicating money

The safe join starts with relationship keys and keeps amounts from exactly one award row:

SELECT
  award.edition_date,
  award.award_id,
  award.piid,
  award.awarding_agency_name,
  award.total_obligated_amount_usd,
  supplier.supplier_id,
  supplier.recipient_name,
  rel.relationship_method,
  award.source_url
FROM award_suppliers AS rel
JOIN awards AS award
  ON award.award_id = rel.award_id
 AND award.supplier_id = rel.supplier_id
JOIN suppliers AS supplier
  ON supplier.supplier_id = rel.supplier_id;
Python join gatesValidate cardinality before attaching supplier context.
Key · cardinality · assertion
Dataset products, join keys, expected cardinalities, and required rejection checks
BoundaryKeyExpected cardinalityGate
01Award rowsaward_idUnique inside one editionReject duplicate award summaries before any amount aggregation.
02Supplier lookupsupplier_idOne lookup row per supplier_idApply UEI → CAGE → scoped fallback identity priority; never use recipient_name as an entity key.
03Award → supplieraward_id + supplier_idExactly one prime-recipient link per edition awardDo not interpret the link as a subcontractor relationship.
04Across editionsaward_idChoose one desired snapshotDo not sum the same cumulative award balance from overlapping releases.
05Official verificationsource_urlOne USAspending award permalinkOpen the official USAspending page, then verify consequential contract terms in SAM.gov or agency records.
  1. Step 01Load

    Read all keys and amount columns as strings.

  2. Step 02Assert

    Check 17,945 unique award IDs and 17,945 relationship rows.

  3. Step 03Join

    Attach 3,145 supplier identities with a many-to-one validated merge.

  4. Step 04Aggregate

    Sum award-grain values only after the row count still reconciles.

Final invariant: enriching 17,945 award rows with supplier attributes must still return 17,945 award rows. A larger result means the join changed the grain.

Before aggregating, assert that the join returns 17,945 rows and 17,945 unique award IDs. If a later relationship model produces multiple rows per award, keep the amount at award grain. Select the one relationship needed for the question, or allocate only under an explicit, validated rule whose allocated values reconcile to the original award amount.

suppliers.csv already contains award_count and total_obligated_amount_usd derived from the awards represented in this edition. Use it as a convenience roll-up or recompute from awards as a reconciliation check. Never add the supplier total to the award total: they describe the same award-level obligations at two grains.

A supplier review queue can count changed awards safely:

SELECT
  supplier.supplier_id,
  supplier.recipient_name,
  count(*) AS awards_in_changed_window,
  count(DISTINCT award.awarding_agency_code) AS awarding_agencies,
  sum(try_cast(
    award.total_obligated_amount_usd AS DECIMAL(38, 2)
  )) AS cumulative_obligations_on_those_awards
FROM awards AS award
JOIN suppliers AS supplier
  ON supplier.supplier_id = award.supplier_id
GROUP BY supplier.supplier_id, supplier.recipient_name
ORDER BY awards_in_changed_window DESC, supplier.supplier_id;

That output is a change-monitoring queue. It is not an all-time supplier portfolio, corporate-family total, revenue ranking, or market-share table.

Measure field coverage before ranking

Every filter changes the denominator. Measure present, blank, and parseable values before using a field as a ranking dimension:

SELECT 'outlay' AS field,
       count(*) FILTER (WHERE total_outlay_amount_usd <> '') AS present_rows
FROM awards
UNION ALL
SELECT 'offers_received',
       count(*) FILTER (WHERE number_of_offers_received <> '')
FROM awards
UNION ALL
SELECT 'set_aside',
       count(*) FILTER (WHERE set_aside_code <> '')
FROM awards
UNION ALL
SELECT 'naics',
       count(*) FILTER (WHERE naics_code <> '')
FROM awards
UNION ALL
SELECT 'psc',
       count(*) FILTER (WHERE product_or_service_code <> '')
FROM awards;

The inaugural edition has outlays on 1,580 rows, offers received on 11,758 rows, and a set-aside code on 8,714 rows. NAICS and PSC are present on all 17,945 rows. Presence still does not prove that a code or value is correct for the analytical claim.

Composition can be extreme in a short changed-record window. General Services Administration is the awarding agency on 12,657 rows—70.53% of this edition. That is a useful pipeline-composition check. It is not evidence that GSA held 70.53% of federal contract awards, obligations, or market demand.

The same boundary applies to award types. Reproduce the composition check from the verified awards table:

SELECT award_type, count(*) AS changed_award_records
FROM awards
GROUP BY award_type
ORDER BY changed_award_records DESC, award_type;

The result is 7,812 purchase orders, 5,081 delivery orders, 4,752 BPA calls, and 300 definitive contracts. Those are current award-summary types among records modified in the selected window, not a league table of every federal contract.

For procurement market sizing and supplier concentration, define a representative period and transaction or award cohort first, then follow the government contract analysis method. Do not promote a collector window into a market denominator.

Export bounded results with a provenance receipt

An analytical handoff needs evidence about the result, not only about the input. The downloadable recipe stages its CSV outputs, sorts them deterministically, computes each output's SHA-256, then writes the provenance receipt last.

The tested run writes five bounded tables—changed-window-checkpoints.csv, changed-window-agency-record-counts.csv, changed-window-field-coverage.csv, changed-window-supplier-review.csv, and changed-window-join-validation.csv—followed by changed-window-query-provenance.json. The agency and supplier views are capped and contain record counts rather than amount rankings.

The generated receipt uses this structure (abridged):

{
  "dataset_id": "us-federal-contract-awards",
  "recipe": "us-federal-contract-awards-python",
  "recipe_version": "1.0",
  "release_tag": "2026-08-25",
  "changed_window": {
    "date_field": "last_modified_date",
    "start": "2026-08-23",
    "end": "2026-08-25",
    "inclusive": true
  },
  "duckdb_version": "1.5.5",
  "inputs": {
    "manifest.json": {
      "bytes": 11153,
      "sha256": "fcfe470f...443c7e"
    }
  },
  "outputs": {
    "changed-window-agency-record-counts.csv": {
      "rows": 25,
      "bytes": 2615,
      "sha256": "..."
    }
  },
  "interpretation": {
    "changed_window": "Rows were updated in the source window; this is not period spend or market share."
  }
}

Keep the release window and amount interpretation beside each CSV, not only in a README that can be separated from it. If an export is filtered to an agency, supplier, NAICS, PSC, geography, competition type, or set-aside, record the exact filter and unmatched/null denominator.

For recurring ingestion, retain editions as immutable observations and build a separate current-state table keyed by award_id. Upsert a newer desired snapshot; do not append overlapping award summaries and sum them. If you need the individual actions that produced a net summary change, collect the USAspending transaction history instead.

Choose the public dataset, USAspending API, or official record

These routes serve different jobs:

  • Use the free WebTruffle dataset for normalized, account-free award, supplier, and relationship files with release checksums and a narrow public scope.
  • Use the USAspending API when you need source-side filtering, transaction histories, IDVs, generated downloads, or a recurring integration you control. The USAspending API guide covers request design, pagination, awards versus transactions, and bulk options.
  • Use the SAM.gov Contract Awards API when the job is FPDS-era contract-action integration, modification context, IDV relationships, recently deleted contracts, or migration from legacy FPDS Atom feeds. The official field-variance guide documents API differences. A SAM.gov account and API key are required; use the FPDS migration guide for that cutover.
  • Open an award row's source_url for the current official USAspending page. For decision-critical contract terms, verify the corresponding SAM.gov or awarding-agency record; a verified derivative remains a derivative. For a browser-first investigation, use the manual awarded-contract research workflow.

USAspending explains that its procurement data is assembled from agency contract reporting and can be delayed, corrected, or linked after initial publication. FAR 4.604 generally requires a Contract Action Report within three business days, with a 30-day deadline for specified urgent or emergency actions. USAspending documents next-day ingestion and following-day publication after FPDS submission, plus a 90-day FPDS submission delay for Department of Defense and US Army Corps of Engineers procurement records. (USAspending data sources and timing; FAR 4.604)

A healthy source-health file proves that this collector's request succeeded and reconciled. It is not a completeness watermark for the federal source.

US federal awards Python checklist

  • [ ] Pin the release tag, manifest bytes, and independently recorded manifest SHA-256.
  • [ ] Verify all selected data-file bytes and hashes before opening DuckDB.
  • [ ] Require the exact 66, 13, and 10-field headers with UTF-8 BOM handling.
  • [ ] Reconcile the source count equation and each product's declared row count.
  • [ ] Require unique award_id and supplier_id keys at their declared grains.
  • [ ] Prove relationship rows have no orphan award or supplier keys.
  • [ ] Read all source columns as text; cast identifiers never and amounts only for one query.
  • [ ] Keep edition, modified, base-action, latest-action, performance, and observation dates separate.
  • [ ] Aggregate cumulative amounts once from the awards table.
  • [ ] Preserve missing outlays as missing and reported zeroes as zero.
  • [ ] Label agency, supplier, category, and geography outputs as changed-window composition.
  • [ ] Use transactions—not award-summary snapshots—for fiscal-period obligation flows.
  • [ ] Sort bounded exports deterministically and hash them.
  • [ ] Write the provenance receipt only after every output passes its checks.
  • [ ] Follow source_url, then verify SAM.gov or awarding-agency records before a time-sensitive, legal, compliance, or bid decision.

Limitations and interpretation boundaries

  • The public repository has one release as of this article. “Daily” describes the designed cadence, not an observed long-run reliability record.
  • The release is verifiable from public artifacts, but the private transient source archive and collector are not published; this guide does not claim end-to-end rebuild reproducibility.
  • The edition is a rolling three-day change window, not a complete active-contract inventory or contract-action ledger.
  • The April 4, 2022 base-action cutoff excludes earlier awards and reduces legacy D&B-derived identity provenance risk. It does not create a complete post-cutoff inventory or a blanket public-domain license.
  • The inaugural edition uses UEI for every supplier identity. It does not empirically test future CAGE-only or award-scoped unresolved fallback records.
  • Recipient names are source-published labels. The dataset does not infer corporate parents, DBAs, successor entities, or fuzzy same-name matches.
  • Supplier relationships identify current prime recipients, not subcontractors or historical recipient changes.
  • Missing outlay, offer, solicitation, set-aside, or geography data must remain unknown unless another verified source supplies it.
  • Amounts are cumulative award-summary measures. Differences between snapshots are net summary differences, not proof of one underlying modification.
  • NAICS is the reported principal industry code for the acquisition; PSC identifies the predominant product or service purchased. Neither is a complete supplier-industry profile.
  • Place of performance is not recipient headquarters or a complete list of work sites.
  • A row absent from an edition can mean no recent modification, a reporting delay, the public cutoff, or an excluded award type. It does not prove that no contract exists.
  • The source maximum update date is advisory. A healthy run does not prove source completeness, especially where delayed public reporting applies.
  • The awards product's source_license value is a source-notice URL, not a blanket license grant. Field-specific or third-party terms may apply; review the pinned SOURCE_NOTICES.md and verified manifest before redistribution.

Frequently asked questions

Is this a daily list of newly awarded federal contracts?

No. Each edition is a rolling three-day window of USAspending prime-award summaries selected by source last_modified_date. The inaugural edition has 16,825 awards whose base action predates that window. Use base_action_date for the original award date and transactions for individual action history.

Why does every first-release row say change_type=new?

Because the first release is the pipeline's baseline. Every retained award is new to that observation history. The value does not mean every contract was newly signed, newly obligated, or newly paid during the edition.

Can I sum total_obligated_amount_usd?

You may sum it once per unique award_id to describe cumulative obligations on the awards represented in one edition or a deliberately selected current snapshot. Label that population precisely. Do not call the result dollars obligated during the edition, and never sum overlapping snapshots or relationship rows.

Is a missing outlay the same as zero?

No. In the pinned edition, only 1,580 of 17,945 awards have a reported outlay value. Blank means missing or not reported in this product; a literal 0 is a reported zero. Preserve that distinction in both SQL and exports.

Which field should I use to join awards?

Use the opaque award_id at award grain and supplier_id at normalized supplier grain. Keep piid, UEI, CAGE, and recipient name as useful attributes with their documented scope. Do not reconstruct award_id or join entities by name alone.

Can I use recipient_name to count federal contractors?

No. Names can vary, collide, and change. Use supplier_id, understand whether it is based on UEI, CAGE, or an award-scoped unresolved key, and do not interpret the result as a corporate-family count. This first edition happens to use UEI for all 3,145 supplier identities.

How should I combine multiple daily editions?

Retain each edition as an immutable observation, then select the desired snapshot per award_id for current-state analysis. Do not append overlapping editions and sum cumulative amounts. Compare content hashes or fields to detect net summary changes; use transaction data to explain the individual actions behind them.

Can this dataset produce fiscal-year federal spending totals?

Not by itself. A fiscal-year obligations analysis needs transaction-level federal_action_obligation values grouped by transaction action date. Filtering award summaries by base action date and summing cumulative obligations answers a cohort question, not an in-year spending-flow question.

Why use DuckDB instead of pandas?

DuckDB can scan, cast, join, group, and export the verified local CSVs without creating a second full dataframe copy. Reading all columns as VARCHAR also keeps identifier and decimal inference explicit. Pandas remains useful after a query narrows the result to a small analytical table.

Does a matching SHA-256 prove the federal data is complete?

No. It proves the local file is byte-for-byte identical to the pinned published artifact. Source completeness, reporting latency, field correctness, identity quality, and analytical interpretation require separate evidence and checks.

Is WebTruffle the authoritative contract record?

No. WebTruffle normalizes and packages public USAspending data. Use the row's source_url for the official USAspending page, then verify decision-critical contract terms against the corresponding SAM.gov or awarding-agency record. Review the pinned source notices before redistribution.