# Federal Contract Leads – SAM.gov & USAspending (`staticxviper/gov-contracts`) Actor

SAM.gov contract opportunities and USAspending contract awards by NAICS, state, set-aside, and date. Official APIs only. No proxy and no CAPTCHA solver.

- **URL**: https://apify.com/staticxviper/gov-contracts.md
- **Developed by:** [CJ Moulton](https://apify.com/staticxviper) (community)
- **Categories:** Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 sam.gov opportunities

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Federal Contract Leads – SAM.gov & USAspending

Contract opportunities from the SAM.gov Get Opportunities API and contract awards from the USAspending spending-by-award API. One dataset row per notice or award, with the id, agency, title, NAICS, value, deadline, awardee, published contact, and a link back to the official page.

The Actor calls two official JSON APIs over plain HTTP. It does not launch a browser, use a proxy, or solve CAPTCHAs. Requests run one at a time with a jittered pause.

### What it does

| Source | Endpoint | What is kept |
|--------|----------|----------------|
| SAM.gov opportunities | `GET https://api.sam.gov/opportunities/v2/search` | `noticeId`, title, agency path, NAICS, set-aside, response deadline, award amount and awardee when the notice includes them, primary point of contact |
| USAspending awards | `POST https://api.usaspending.gov/api/v2/search/spending_by_award/` | Contract awards only (types A, B, C, D): award id, recipient, amount, awarding agency, NAICS, description, start and end dates |

SAM.gov `postedFrom` and `postedTo` are sent as `MM/dd/yyyy`. The same inclusive dates are the USAspending action-date window. SAM.gov is queried once per NAICS code (`ncode`, exact). USAspending receives every code in one `naics_codes.require` filter, which matches codes that start with the value. `offset` on SAM.gov is the page index starting at 0, not a record offset.

Rows are deduped on `(source, recordType, id)`. For an award the id used for dedupe is `generated_internal_id` when USAspending sends one, and the public `Award ID` is what the row shows. A page whose ids were all seen already stops that query.

`maxItems` counts successful rows only. Once it is filled, later pages and the remaining source are not requested.

USAspending search does not publish a person, email, or phone. Those contact fields are empty on award rows. The recipient name and UEI are the published awardee fields. SAM.gov contact fields are copied from the primary point of contact when the notice includes one.

The public opportunity URL is `https://sam.gov/opp/{noticeId}/view`, built from the notice id. A `beta.sam.gov` `uiLink` is not stored. The award URL is `https://www.usaspending.gov/award/{generated_internal_id}` when that id matches a safe token. Award `state` is the place-of-performance filter you set, because this search response does not include a state field. An opportunity `state` is the notice's own place of performance.

### Input

`fixtures/INPUT.json` is the CI fixture:

```json
{
  "sources": ["awards"],
  "naics": ["541512"],
  "state": "VA",
  "postedFrom": "2024-01-01",
  "postedTo": "2024-03-31",
  "maxItems": 2,
  "delaySeconds": 1,
  "maxSpendUsd": 1
}
```

Awards only, so the Docker smoke does not call `api.sam.gov` and does not need a SAM key. SAM.gov parsing, the missing-key skip, and a SAM.gov block are covered by fixture unit tests (`unittests/apify_scripts/fixtures/gov_contracts/sam_page.json`), not by this smoke run. The fixture asks for two historical USAspending awards (NAICS 541512, Virginia, first quarter of 2024). `fixtures/budget.json` expects exactly two dataset rows and a $0.016 charge. A run that only pushes a status row fails that gate.

- `sources` is `opportunities`, `awards`, or both. Default is both. Order is preserved.
- `naics` is 2 to 6 digits, at most 10. `state` is a 2-letter USPS code, or empty for every state. At least one of the two is required.
- `setAside` is one SAM.gov set-aside code (`SBA`, `WOSB`, `8A`, and the rest of the GSA list). Empty means every set-aside. SAM.gov accepts one code per request.
- `postedFrom` and `postedTo` are `YYYY-MM-DD`. Both empty means the 90 days ending today. The span cannot exceed 366 days. A bad or one-sided range is one error row and no request.
- `maxItems` caps successful rows after dedupe (default 50, maximum 200). It also bounds the network: later pages are not requested once it is filled. `maxSpendUsd` is applied after the fetch, when rows are pushed.
- `samApiKey` is the SAM.gov public API key from Account Details. It is a secret input. It is sent only as `api_key` on `api.sam.gov` and is redacted from stored URLs, errors, the dataset, and `SUMMARY`. Leave it empty to skip opportunities: one free `skipped` row is saved and awards still run.
- `delaySeconds` is the base pause between requests after the first (default 1, maximum 10). The pause jitters upward by up to 1 second.
- `maxSpendUsd` defaults to 5 (minimum 0.01, maximum 100). A lead is pushed only when its `opportunity` or `award` charge fits. The feed then stops with one free `spend_capped` row.

### Output

One dataset item per opportunity or award, plus a status row when a source is skipped, blocked, disallowed, or failed. Key-value `SUMMARY` records counts (`found`, `billedOpportunities`, `billedAwards`, `heldBack`, `skipped`, `blocked`, `errors`, `robots`, `mergedDuplicates`, `truncated`, `chargeCapped`), the filters, spend, and one report per source (`source`, `status`, `fetched`, `kept`). `SUMMARY` does not contain the API key.

```json
{
  "id": "abc123notice",
  "recordType": "opportunity",
  "source": "sam.gov",
  "agency": "GENERAL SERVICES ADMINISTRATION.PUBLIC BUILDINGS SERVICE",
  "subAgency": "",
  "title": "Historic Office Renovation",
  "naics": "236220",
  "value": 800620,
  "deadline": "2024-03-15T17:00:00-04:00",
  "awardee": "D.G. Beyer, Inc.",
  "contactName": "Casey Officer",
  "contactEmail": "co@example.com",
  "contactPhone": "2025550100",
  "uei": "025114695AST",
  "setAside": "SBA",
  "state": "WI",
  "noticeType": "Solicitation",
  "solicitationNumber": "47PF0018R0023",
  "postedDate": "2024-02-01",
  "periodStart": "",
  "url": "https://sam.gov/opp/abc123notice/view",
  "sourceStatus": "ok",
  "decision": "opportunity",
  "ok": true,
  "error": "",
  "scrapedAt": "2026-11-03T12:00:00+00:00"
}
```

`value` is null when the notice or award does not publish an amount. `deadline` is the SAM.gov response deadline, or the USAspending period-of-performance end date. `periodStart` is the award start date.

| sourceStatus | Meaning | Charged |
|--------------|---------|---------|
| `ok` | An opportunity or award was returned. Pushed only when `maxSpendUsd` covers that row's charge | yes |
| `skipped` | Opportunities were requested and `samApiKey` is empty. No SAM.gov request. Awards still run | no |
| `not_found` | Both sources returned no rows | no |
| `blocked` | HTTP 401, 403, or 429, or a challenge page. That source stops. The other source still runs | no |
| `spend_capped` | `maxSpendUsd` or the platform charge limit ended the feed | no |
| `robots` | robots.txt disallows the URL, or robots.txt could not be read (5xx or network). That host is skipped | no |
| `error` | Bad input, invalid JSON, or a 5xx / network error after one retry. The other source still runs | no |

### Limitations

- SAM.gov opportunities require the caller's own free API key. Daily limits depend on the SAM.gov account role. This Actor does not share a key.
- USAspending rows are prime contract awards (types A, B, C, and D), sorted by award amount descending. Grants and IDVs are not requested.
- At most 5 pages per query, 100 records per page, and 200 successful rows per run.
- A response larger than 32 MB is rejected for that request.
- On HTTP 401, 403, or 429 the Actor pushes one `blocked` row for that source and does not retry it. It does not send the request through a proxy.
- A 5xx or a dropped connection is tried once more. The second failure is an `error` row for that source.
- `maxSpendUsd` is checked before the row is pushed. A lead that would pass the cap is not pushed. The HTTP fetch has already finished by then, so the cap does not cancel a request that `maxItems` already allowed.
- If the platform charge raises after a row was pushed, that row stays in the dataset and is not billed. The feed stops. The row is not deleted and it is not charged again.

### Robots and terms

robots.txt is fetched once per host and evaluated with `scripts/lead_automation/crm_enrich_robots.py` (longest match, `User-agent: *`). A 4xx robots answer, including the 404 these API hosts return, means no rules. A 5xx or network failure skips that host for the run and records a `robots` row. A disallow is a `robots` row and the API URL is not fetched.

The data is published by SAM.gov and USAspending for public use. Contact fields are copied only when the API publishes them. Award rows do not invent a person, email, or phone.

### Pricing

Pay per event from `.actor/pay_per_event.json`.

| Event | Price | When it is charged |
|-------|-------|--------------------|
| `opportunity` (primary) | $0.008 | A SAM.gov opportunity row is pushed |
| `award` | $0.008 | A USAspending award row is pushed |

**$0.008 is $8 per 1,000 rows.** The brief suggested about $0.01, which is $10 per 1,000 and above the listing's affordability line. Both events use $0.008. No incumbent price was verified for this build. There is no `proxy` or `captcha` event, because an undeclared event is priced at $1 in the Docker smoke and this Actor never uses either.

`skipped`, `not_found`, `blocked`, `robots`, `spend_capped`, and `error` rows are saved and not billed. Dataset items are not a charge event. The platform still applies its start event. Memory default is 512 MB (minimum 256, maximum 2048) and the default timeout is 1,800 seconds.

#### Deviation from the template contract

The template keeps `headless`, `maxProxyCostUsd`, `allowCaptcha`, `proxyConfiguration`, and `proxyGroups` in every input schema. This Actor drops them. It reads official JSON APIs with plain HTTP, never launches a browser, and stops a source on a block instead of spending to get past it. Only `maxSpendUsd` is kept.

The Dockerfile uses `apify/actor-python:3.12` instead of the shared Playwright image. It still leaves `WORKDIR` at `/usr/src/app`, copies only files inside this folder, and sets `CJM_REPO_ROOT=/usr/src/app/platform_bundle`. The slim image has no `xvfb-entrypoint.sh`.

`LeadLedger` counts micro-dollars and does not price `proxy` or `captcha`. $0.008 is 0.8 cents, so the shared cent ledger would round it to 1 cent. The micro-dollar ledger is what the push path checks before a row is saved.

### Categories

Set in the Apify Console, not in `actor.json`. Suggested: Lead generation, Business.

### Local run

```bash
cd scripts/apify-scripts/gov-contracts
mkdir -p storage/key_value_stores/default
cp fixtures/INPUT.json storage/key_value_stores/default/INPUT.json
apify run --purge
```

That sample is the CI fixture: two USAspending awards, no SAM.gov call. Add `samApiKey` and include `opportunities` in `sources` to read SAM.gov.

Unit tests read fixtures only. They do not call `api.sam.gov`, `api.usaspending.gov`, or Apify.

```bash
python -m pytest unittests/apify_scripts/test_gov_contracts_actor.py -q
```

The Apify Actors workflow builds this image and runs `fixtures/INPUT.json` against `fixtures/budget.json`. Publish with `APIFY_TOKEN` (`apify push`) only after a listing is requested. See `scripts/apify-scripts/README.md`.

# Changelog

This Actor's version history is a separate document: https://apify.com/staticxviper/gov-contracts/changelog.md

# Actor input Schema

## `sources` (type: `array`):

opportunities calls SAM.gov. awards calls USAspending for contract awards (types A, B, C, and D). Default is both. A block stops that source only.

## `naics` (type: `array`):

2 to 6 digit NAICS codes. SAM.gov is queried once per code (ncode). USAspending matches codes that start with any listed code. At most 10. Leave empty only when a state is set.

## `state` (type: `string`):

Place of performance. Two-letter USPS code. Empty searches every state. SAM.gov receives it as state. USAspending receives it as a USA place-of-performance location.

## `setAside` (type: `string`):

One SAM.gov set-aside code. Empty means every set-aside. Sent as typeOfSetAside to SAM.gov and as set_aside_type_codes to USAspending. Only one code is accepted.

## `postedFrom` (type: `string`):

YYYY-MM-DD, inclusive. SAM.gov postedFrom (sent as MM/dd/yyyy). USAspending action-date start. Both ends are required together. Empty uses the 90 days ending today. The span cannot exceed 366 days.

## `postedTo` (type: `string`):

YYYY-MM-DD, inclusive. SAM.gov postedTo. USAspending action-date end. Empty uses today when postedFrom is also empty.

## `maxItems` (type: `integer`):

Hard cap on successful opportunity and award rows after dedupe. Status rows do not count and are not billed. Later pages and the remaining source are not requested once the cap is filled.

## `samApiKey` (type: `string`):

Public API key from the SAM.gov Account Details page. Required for opportunities. Stored as a secret and sent only to api.sam.gov. Leave it empty to collect USAspending awards only.

## `delaySeconds` (type: `number`):

Seconds to wait before each request after the first. Requests run one at a time. The pause jitters upward by up to 1 second.

## `maxSpendUsd` (type: `number`):

A lead is pushed only when its opportunity or award charge fits under this amount. When the next lead would pass it, the feed stops and one free spend_capped row records how many leads were held back. Default 5, minimum 0.01, maximum 100.

## Actor input object example

```json
{
  "sources": [
    "opportunities",
    "awards"
  ],
  "naics": [
    "541512"
  ],
  "state": "",
  "setAside": "",
  "maxItems": 50,
  "delaySeconds": 1,
  "maxSpendUsd": 5
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset containing all scraped items

## `files` (type: `string`):

Key-value store containing the SUMMARY record for this run

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "naics": [
        "541512"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("staticxviper/gov-contracts").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "naics": ["541512"] }

# Run the Actor and wait for it to finish
run = client.actor("staticxviper/gov-contracts").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "naics": [
    "541512"
  ]
}' |
apify call staticxviper/gov-contracts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,staticxviper/gov-contracts"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ciqe95ByNB6avrpSs/builds/WxFY99lDqf8wy9Ynu/openapi.json
