Keeping an agent inside its budget
The pattern that stops one ambitious prompt spending a month of credits.
The failure this prevents
A user asks an agent for "every apartment project in the thousand largest cities". A naive agent turns that into a thousand metered calls. The API will refuse before it overspends — a paid plan is never partially delivered, so the agent gets a 402 rather than a surprise invoice — but a thousand 402s is a bad afternoon for everyone.
The pattern
- 1The agent's first call for any new task is
GET /projects/count. It is free, it works at a zero balance, and it returns everything needed to decide:matchCount,billable.credits,affordability.affordableandaffordability.maxAffordableRecords. - 2If
affordableis false, the agent narrows and counts again rather than attempting the call. Narrowing is free; attempting is a rate-limit slot. - 3When the agent does spend, it reads
X-Credits-Remainingoff the response rather than maintaining its own counter, because a teammate or another agent may be spending the same pool. - 4On a
402, the agent followserror.details.quote.countUrl— a free URL that prices the exact filter set that just failed — instead of retrying blindly. - 5For questions about shape rather than records, the agent uses the analytics plane, which is free.
Price a broad query — before spending anything
curl 'https://api.boardwalkai.com/api/v1/projects/count?states=UT&assetClasses=Apartment%20Building&dateRange=all' \
-H 'Authorization: Bearer bwk_live_YOUR_KEY'Response · HTTP 200
{
"data": {
"matchCount": 5000,
"matchCountIsExact": true,
"matchCountBasis": "enumerated",
"countedAs": "distinct_projects",
"scanCeiling": 10000,
"billable": {
"records": 5000,
"credits": 5000,
"creditRate": 1,
"isUpperBound": true,
"nonBillable": {
"alreadyDeliveredUnchanged": 0,
"ledgerApplied": true
}
},
"affordability": {
"affordable": false,
"blockReason": "insufficient_credits",
"shortfallCredits": 4500,
"maxAffordableRecords": 500,
"boundBy": "period_balance",
"unlimited": false
},
"ordering": {
"sort": "meetingDate",
"order": "desc",
"tiebreak": null,
"meaning": "Most recent meeting evidence first — the date of the meeting the record was extracted from, not when Boardwalk ingested it and not when the project was created."
},
"truncation": {
"policy": "refuse",
"wouldTruncate": false,
"deliverableRecords": null,
"explanation": "Paid plans are never partially delivered. A request that costs more than the balance is refused with 402 and a quote; narrow the filters or top up."
}
},
"meta": {
"schemaVersion": "2026-08-02",
"representation": "standard",
"deliveryMode": "all",
"credits": {
"limit": 500,
"used": 0,
"remaining": 500,
"charged": 0,
"periodStart": "2026-08-01",
"periodEnd": "2026-08-31",
"isTeamPool": true
},
"appliedFilters": {
"states": [
44
],
"assetClasses": [
"Apartment Building"
],
"dateRange": "all"
},
"appliedDefaults": [],
"resolvedFrom": {
"states": {
"UT": 44
}
},
"warnings": [
{
"code": "filter_evidence_unconfirmed",
"message": "These filters currently match our full index. A returned record with a null value for the attribute you filtered on is a record we could not confirm.",
"params": [
"assetClasses"
]
}
]
},
"links": {
"self": "https://api.boardwalkai.com/api/v1/projects/count?states=UT&assetClasses=Apartment%20Building&dateRange=all",
"search": "https://api.boardwalkai.com/api/v1/projects/search?states=UT&assetClasses=Apartment%20Building&dateRange=all",
"docs": "https://boardwalkai.com/docs/api/budgets/"
}
}The response shape on this page is the one the endpoint builds — the keys, their nesting and their types are asserted against the shipping code by a test. The numbers inside it are the scenario being walked through, not a measurement of the corpus.
Rules of thumb for tool descriptions
- Tell the model that counting is free and searching is not. Models respect a stated cost asymmetry.
- Give the model
representation: "compact"as its default. It is the same price and roughly a fifth of the fields, which matters for context, not for cost. - Never let a model choose a
limitabove what one task needs. The page ceiling is 100 records; a loop over pages should be a deliberate decision, not a default. - Surface
meta.warningsto the model. Afilter_evidence_unconfirmedwarning is why an attribute it filtered on came back empty, and without it the model will report a data bug to its user.
Costs at a glance
Billable — 1 credit per record delivered
| Method | Path | Cost | Notes |
|---|---|---|---|
| GET | /alerts/{alertId}/matches | 1 credit / record | Records already delivered and billed by a scheduled run are re-readable here without a second charge; anything new to you bills once. |
| POST | /alerts/{alertId}/run | 1 credit / record | A manual run is a real delivery: records new to you bill at the standard rate. Runs are capped per alert per day. |
| GET | /documents/{id} | 1 credit / record | One source document, one credit. |
| POST | /projects/ai-search | 1 credit / record | The same rate as a structured search. You pay for confirmed matches that are delivered, not for the candidates that were reviewed and rejected. |
| POST | /projects/ai-search/jobs | 1 credit / record | Credits are reserved up front against the requested limit and settled down to the number of confirmed records actually delivered. |
| GET | /projects/search | 1 credit / record | Charged for the records actually delivered on the page, after duplicate projects are collapsed and after records you already hold unchanged are excluded. Zero results cost nothing. |
| GET | /projects/sync | 1 credit / record | Only records that are new to you, or that changed since you last received them, are billable. A sync page that returns nothing but unchanged records is free. |
| GET | /projects/{id} | 1 credit / record | One record, one credit — the same rate as a record inside a search page. |
Free — 0 credits
| Method | Path | Cost | Notes |
|---|---|---|---|
| GET | /account/credits | Free | Free. Your balance, your plan and your rate limit. |
| GET | /alerts | Free | Free. Configuring alerts never costs credits; only delivered records do. |
| POST | /alerts | Free | Free. |
| GET | /alerts/{alertId} | Free | Free. |
| PATCH | /alerts/{alertId} | Free | Free. |
| DELETE | /alerts/{alertId} | Free | Free. |
| GET | /alerts/{alertId}/deliveries | Free | Free. The delivery history and what each run charged. |
| GET | /alerts/{alertId}/deliveries/{deliveryId} | Free | Free. |
| POST | /alerts/{alertId}/dismissals/{projectId} | Free | Free. Dismissing a match stops it being re-offered. |
| DELETE | /alerts/{alertId}/dismissals/{projectId} | Free | Free. |
| POST, GET | /alerts/{alertId}/preview | Free | Free. Preview returns how many records a rule would deliver and what that would cost, not the records themselves. |
| POST | /analytics/aggregate | Free | Free. Returns counts and group keys, never a project id, name, address, contact or document. |
| GET | /analytics/datasets | Free | Free. What the analytics plane can and cannot answer, self-described. |
| POST | /analytics/rankings | Free | Free, and subject to the same small-group suppression floor as /aggregate. |
| GET | /health | Free | Free and unauthenticated. |
| GET | /locations/cities/{id} | Free | Free. |
| GET | /locations/counties/{id} | Free | Free. |
| GET | /locations/states | Free | Free. Resolve the ids you filter with. |
| GET | /locations/states/{id} | Free | Free. |
| GET | /locations/states/{id}/counties | Free | Free. |
| GET | /locations/states/{stateId}/counties/{countyId}/cities | Free | Free. |
| GET | /projects/ai-search/jobs/{id} | Free | Polling is free. The records the job delivers were charged when the job settled, not when you read them. |
| GET | /projects/count | Free | Free, always, including when your balance is zero. This is the endpoint that tells you what a query would cost before you buy it. |
| GET | /taxonomy | Free | Free. The complete land use taxonomy tree. |
| GET | /taxonomy/action-categories | Free | Free. |
| GET | /taxonomy/action-types | Free | Free. |
| GET | /taxonomy/asset-classes | Free | Free. |
| GET | /taxonomy/project-types | Free | Free. |
| GET | /taxonomy/subtypes | Free | Free. |
Next
Ready to make a call?
A free Boardwalk trial includes API access and a sandbox key. Counting, filtering, the location tree and the analytics plane cost nothing, so you can evaluate the data before you spend a credit.