# State & Local Bid Monitor — BidNet, Texas, NY, PlanetBids (`chimerical_quicklime/state-procurement-monitor`) Actor

Watch state and local government bids on BidNet Direct (47 states), Texas ESBD, NY Contract Reporter, PlanetBids and Bonfire and get only NEW or changed solicitations since the last run: due date, agency, category, contact, value. Daily schedule. No login. MCP-ready. $10 per 1,000 alerts.

- **URL**: https://apify.com/chimerical\_quicklime/state-procurement-monitor.md
- **Developed by:** [Khrystyna Skotte](https://apify.com/chimerical_quicklime) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 bid alerts

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

## State & Local Bid Monitor — new government bids daily, one feed

Get a **daily feed of only the new or updated state and local government solicitations** that match your
keywords, pulled from five public procurement portals in one run. The monitor remembers every bid it has
already reported, so a scheduled run emits just the delta (marked `new` or `updated`), posts a summary to your
webhook, and costs you only for what actually changed. No login, no API key, no proxy required.

Use it instead of checking BidNet, the Texas ESBD, the NY Contract Reporter and each city's PlanetBids or
Bonfire portal by hand every morning.

### Source coverage

| `sources` value | Portal | Coverage | Fields beyond title/dates | Filters applied server-side |
|---|---|---|---|---|
| `bidnet` | [BidNet Direct](https://www.bidnetdirect.com) state purchasing groups | 47 states (all except CO, VA, WY; MI via the MITN group), thousands of local agencies | AI summary of the notice as `description`, state | keyword, state |
| `texas-esbd` | [Texas Electronic State Business Daily](https://www.txsmartbuy.gov/esbd) | Every Texas state agency, university, ISD and co-op member posting on ESBD | agency, NIGP codes, description, contact name/email, estimated value | posting-date window |
| `nyscr` | [NYS Contract Reporter](https://www.nyscr.ny.gov) | New York State agencies, authorities, SUNY/CUNY, NYC-adjacent public bodies | agency, division, category, ad type, location | posting-date window |
| `planetbids` | [PlanetBids](https://home.planetbids.com) agency portals | Any agency on PlanetBids (hundreds of California cities, counties and districts); one portal id per agency | agency, NAICS/category ids, description, contact, estimated value, stage | keyword, issue date |
| `bonfire` | [Bonfire](https://gobonfire.com) agency portals | Any agency on Bonfire (cities and counties nationwide); one subdomain per agency | agency, reference number, closing date | none (all open bids listed) |

Default `{}` run: `bidnet` for California and Texas plus `texas-esbd`, last 7 days, up to 5 bids per source.

**Sources that were checked and skipped** (not reachable without a browser or login, so not included):
Cal eProcure (PeopleSoft, browser only), DemandStar (API requires an account token), OpenGov Procurement
(Cloudflare challenge), Maryland eMMA and Arizona APP (Ivalua browser check / postbacks), Ohio Procure,
Florida VBS, NC eVP, PA eMarketplace, Virginia eVA (JavaScript-only listings), COMMBUYS, NJSTART, BidBuy
(public bid pages removed).

### How it works

1. For each selected source, lists the bids **posted within the last `lookbackDays`** (Bonfire has no posted
   date, so all of its open bids are considered).
2. Applies your `keywords` and `naicsOrCategories` filters.
3. Compares every match with the monitor's saved state (`source:bidId → due date + status`).
4. Emits a record when the bid is unseen (`changeType: "new"`) or its due date / status changed
   (`changeType: "updated"`), enriched with the detail page (description, contact, estimated value) where the
   portal publishes one.
5. Saves the state, then POSTs a run summary to `webhookUrl` if set.

State lives in a named key-value store `stateproc-monitor-<hash of monitorId>` in your Apify account, capped at
50,000 bids (oldest are dropped). Delete the store to reset a monitor.

### Input

| Field | Default | Notes |
|---|---|---|
| `sources` | `["bidnet","texas-esbd"]` | Any of `bidnet`, `texas-esbd`, `nyscr`, `planetbids`, `bonfire` |
| `states` | `["CA","TX"]` | Picks the BidNet purchasing groups; `texas-esbd` only runs when `TX` is listed, `nyscr` only when `NY` is. Empty = all groups and all state sources |
| `portals` | `[]` | Your agencies: `planetbids:17950`, `bonfire:dallascityhall`, optionally `:CA` to tag the state (see below). When empty, `planetbids` uses four California agencies and `bonfire` uses Dallas, Boulder County and Charlotte as examples |
| `keywords` | `[]` | OR-ed, case-insensitive. Matched on title, reference number, category and description; BidNet and PlanetBids are also searched server-side per keyword. Empty = every bid |
| `naicsOrCategories` | `[]` | Substring match on the category field (NIGP codes on Texas, NAICS ids on PlanetBids, category names on NY). Bids without a category field pass through |
| `lookbackDays` | `7` | Posted within N days. The seen-state prevents duplicates, so a wide overlap is safe |
| `maxNewPerSource` | `5` | Cap per source per run; the rest stays unseen and comes out next run |
| `maxItems` | `10` | Cap per run over all sources |
| `firstRunMode` | `emitAll` | `emitAll` reports every current match on the first run; `baseline` records them silently |
| `webhookUrl` | `""` | Optional POST target for the run summary |
| `monitorId` | `default` | One state store per ID — run several watchlists side by side |
| `proxyConfiguration` | none | Only needed if BidNet's AWS WAF starts challenging (many states × many keywords); then enable residential proxies |

#### Finding your portal ids

- **PlanetBids**: open your agency's vendor portal, e.g. `https://vendors.planetbids.com/portal/17950/bo/bo-search`.
  The number after `/portal/` is the id → `planetbids:17950`. Examples: 17950 City of San Diego, 23532 City of
  Palmdale, 39981 City of Montclair, 48397 Santa Clara Valley Water District.
- **Bonfire**: open the agency's opportunities page, e.g. `https://dallascityhall.bonfirehub.com/portal/?tab=openOpportunities`.
  The subdomain is the id → `bonfire:dallascityhall`. Examples: `dallascityhall`, `bouldercounty`, `charlottenc`,
  `detroit`, `cincinnati-oh`, `utah`.
- Add a state tag to fill the `state` column for portal bids: `planetbids:17950:CA`, `bonfire:dallascityhall:TX`.

### Recommended setup for a daily feed

1. Create a task with your sources, states, portals and keywords, and set `monitorId` (e.g. `roofing-tx-ca`).
2. **First run: set `firstRunMode` to `baseline`** and `lookbackDays` to 30. This records everything currently
   open that matches, emits nothing, and stops your first day from being a 300-row backlog.
3. Switch `firstRunMode` back to `emitAll` (it only matters while the state is empty), keep `lookbackDays` at 7
   and raise `maxNewPerSource` / `maxItems` to 100+.
4. **Schedule the task daily at 07:00 in your time zone.** Portals post throughout the business day; a morning run
   catches the previous day's postings before your estimators start.
5. Point `webhookUrl` at Slack (incoming webhook), Zapier, Make, or your own endpoint.

### Example: roofing contractor watching Texas, California and two home-town portals

```json
{
  "sources": ["bidnet", "texas-esbd", "planetbids", "bonfire"],
  "states": ["TX", "CA"],
  "portals": ["planetbids:17950:CA", "bonfire:dallascityhall:TX"],
  "keywords": ["roofing", "roof replacement", "re-roof"],
  "lookbackDays": 7,
  "maxNewPerSource": 100,
  "maxItems": 500,
  "monitorId": "roofing-tx-ca",
  "webhookUrl": "https://hooks.slack.com/services/..."
}
```

### Output

One record per new or updated bid:

```json
{
  "source": "texas-esbd",
  "portal": "txsmartbuy.gov",
  "agency": "Health & Human Services Commission",
  "state": "TX",
  "bidId": "HHS0017500.v3",
  "referenceNumber": "HHS0017500.v3",
  "title": "As-needed Locksmith and Security Door Installation Services - El Paso State Supported Living Center",
  "description": "HHSC PCS is seeking competitive bids to establish contract(s) for as-needed locksmith ...",
  "category": "91048-Locksmith Services",
  "postedDate": "2026-09-28",
  "dueDate": "2026-10-20T10:30",
  "daysUntilDue": 22,
  "status": "Posted",
  "estimatedValue": 105000,
  "contactName": "Vanessa Cantu",
  "contactEmail": "Vanessa.Cantu@hhs.texas.gov",
  "bidUrl": "https://www.txsmartbuy.gov/esbd/HHS0017500.v3",
  "matchedKeyword": "locksmith",
  "changeType": "new",
  "firstSeenAt": "2026-09-29T00:26:52.932Z",
  "monitorId": "default"
}
```

`dueDate` is the portal's local time without a zone suffix (each portal publishes in its own time zone).
`description`, `estimatedValue`, `contactName` and `contactEmail` are `null` when the portal does not publish
them (BidNet hides the issuing agency and contacts behind registration; NY Contract Reporter and Bonfire show
details only to logged-in vendors). `daysUntilDue` is computed at run time.

### Webhook payload

```json
{
  "monitorId": "roofing-tx-ca",
  "runAt": "2026-09-29T07:00:12.000Z",
  "newCount": 4,
  "updatedCount": 1,
  "scanned": 212,
  "perSource": { "bidnet": { "scanned": 150, "emitted": 3 }, "texas-esbd": { "scanned": 62, "emitted": 2 } },
  "seenTotal": 1830,
  "baseline": false,
  "bids": [ "...first 50 records as above..." ]
}
```

The same summary is saved as `SUMMARY` in the run's default key-value store.

### Pricing

$0.005 per run start plus $0.01 per emitted bid ($10 per 1,000 alerts). Baseline runs and runs with no changes
cost only the start fee. Bids beyond `maxNewPerSource` / `maxItems` are not charged and are reported on the next run.

# Actor input Schema

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

Portals to scan. bidnet = BidNet Direct state purchasing groups (47 states, driven by `states`); texas-esbd = Texas Electronic State Business Daily; nyscr = New York State Contract Reporter; planetbids = PlanetBids agency portals (needs `portals` ids, defaults to 4 California agencies); bonfire = Bonfire agency portals (needs `portals` subdomains, defaults to Dallas, Boulder County, Charlotte).

## `states` (type: `array`):

Two-letter codes. Selects which BidNet Direct purchasing groups to scan and gates the state-specific sources (texas-esbd runs only when TX is listed, nyscr only when NY is). Empty = every BidNet group and all state sources.

## `portals` (type: `array`):

Your own agencies' portals: `planetbids:17950` (the number in vendors.planetbids.com/portal/<id>/...), `bonfire:dallascityhall` (the subdomain in <name>.bonfirehub.com). Add `:CA` etc. to tag the state. Bare numbers are treated as PlanetBids ids, bare words as Bonfire subdomains.

## `keywords` (type: `array`):

OR-ed, case-insensitive. Matched against title, reference number, category and (where available) description; BidNet and PlanetBids are also searched server-side per keyword. Empty = every bid.

## `naicsOrCategories` (type: `array`):

Substring match on the bid's category field (NIGP codes on Texas ESBD, NAICS/category ids on PlanetBids, category names on NY Contract Reporter, department on Bonfire). Bids without a category field are not filtered out.

## `lookbackDays` (type: `integer`):

Only bids posted within this many days are considered (Bonfire has no posted date, so all its open bids are considered). The seen-state prevents duplicates, so 7 gives a safe overlap for a daily schedule.

## `maxNewPerSource` (type: `integer`):

Cap per source per run. Bids beyond the cap stay unseen and come out on the next run.

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

Overall cap on emitted records per run.

## `firstRunMode` (type: `string`):

What to do when the monitor has no saved state yet. emitAll: report every current match as new. baseline: silently record every current match and emit nothing, so the next scheduled run reports only what changed.

## `webhookUrl` (type: `string`):

Optional. After each run a JSON summary {monitorId, runAt, newCount, updatedCount, perSource, bids\[first 50]} is POSTed here (Slack, Zapier, Make, your API).

## `monitorId` (type: `string`):

Name of this watchlist. Each ID keeps its own seen-state, so you can run several monitors side by side.

## `proxyConfiguration` (type: `object`):

Optional. All sources answer without a proxy from Apify's servers. BidNet Direct sits behind AWS WAF and rate-limits a single IP, so if you monitor many states with keywords, or see 'AWS WAF challenge' warnings in the log, enable Apify residential proxies here.

## Actor input object example

```json
{
  "sources": [
    "bidnet",
    "texas-esbd"
  ],
  "states": [
    "CA",
    "TX"
  ],
  "portals": [],
  "keywords": [],
  "naicsOrCategories": [],
  "lookbackDays": 7,
  "maxNewPerSource": 5,
  "maxItems": 10,
  "firstRunMode": "emitAll",
  "webhookUrl": "",
  "monitorId": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `records` (type: `string`):

Dataset of new or updated state/local bids found in this run (JSON).

# 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 = {
    "sources": [
        "bidnet",
        "texas-esbd"
    ],
    "states": [
        "CA",
        "TX"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("chimerical_quicklime/state-procurement-monitor").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 = {
    "sources": [
        "bidnet",
        "texas-esbd",
    ],
    "states": [
        "CA",
        "TX",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("chimerical_quicklime/state-procurement-monitor").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 '{
  "sources": [
    "bidnet",
    "texas-esbd"
  ],
  "states": [
    "CA",
    "TX"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call chimerical_quicklime/state-procurement-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,chimerical_quicklime/state-procurement-monitor"
        }
    }
}
```

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/JxmkTFLvqdXc6Uuz9/builds/n1jGxMEnIQfWkx7jm/openapi.json
