Skip to content

feat(alexandria): provider discovery through Search and billed execution through Scrape - #4621

Open
developersdigest wants to merge 9 commits into
mainfrom
alexandria
Open

feat(alexandria): provider discovery through Search and billed execution through Scrape#4621
developersdigest wants to merge 9 commits into
mainfrom
alexandria

Conversation

@developersdigest

@developersdigest developersdigest commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

Alexandria provider tools on the existing Search and Scrape surfaces, following the frozen Firecrawl/Exchange interface.

  • POST /v2/search with sources: ["alexandria"] (optionally domainTools) returns tool contracts in data.tools, up to limit per discovery source. Discovery is free and never counts toward creditsUsed or the keyless reservation; web results bill as before.
  • POST /v2/scrape with a url accepts domainTools: true (default off) and adds data.tools matched to the scraped page's domain, same shape as Search, free, only for teams with Exchange access.
  • POST /v2/scrape with an alexandria body executes one to ten provider calls and returns data.alexandria with data.creditsCost. /exchange/retrieve shares the same path; its single-call shape relays a provider error with the Exchange's status and code.

Billing

Execution runs inline in the request on existing rails: authorize through Exchange /v1/provider-terms/requirements (404 for unknown providers, so nothing unlisted reaches a quote or execution) against the organization's organizationDataSourceAccess flags, quote, reserve with Autumn (lockCredits, lock alexandria_<chargeId>), execute /v1/retrieve with budget and deadline headers, settle the receipt with finalizeCreditsLock, enqueue the ledger write on the billing queue with a deterministic job id, and report to /v1/usage-events/billing. Paid requests fail closed when authorization, quote, reservation, or USE_DB_AUTHENTICATION is unavailable. No new queue, worker, or migration.

The charge id is sha256(teamId, x-request-id). One Redis record per charge id (7 days) gives: replay of a completed request with its original scrape_id and no second execution, 409 duplicate_request for a different payload, 409 request_in_flight while running, release of the record on any pre-execution refusal so the id can retry, and 503 request_unresolved held for reconciliation when the Exchange may have executed (5xx, timeout, malformed or over-budget receipt, crash after the point of no return). The Exchange has no request-id idempotency, so unresolved requests are never re-executed automatically. A failed ledger commit is refunded at Autumn on the direct route; on the firebill route the durable charge stands pending reconciliation.

Details in apps/api/src/services/alexandria/README.md.

Verification

  • Unit: 47 tests across services/alexandria (retrieve, access), the controller, and search.
  • Typecheck, knip, prettier clean.
  • Live against firecrawl-local (API + TLS Exchange snapshot with the provider-terms, quote, and skills-resolve routes + Exchange Postgres): 17 of 17 smoke checks pass, plus a fetch-harness check that a URL scrape with domainTools: true returns 21 World Bank tools with domain provenance and none without the flag, including one usage row per execution, confirmed billing status on the Exchange, same-id and cross-route replay with a stable scrape_id, concurrent same-id dedupe, 409 codes, unknown-provider refusal before any quote, seeded unresolved and in-flight records, and fail-closed paid execution without Autumn.

Not yet validated

Autumn is not configured locally, so every paid path stops at the reservation. The confirm, the release after an Exchange 4xx, and the ledger enqueue are mock-tested only. One paid call against the Autumn sandbox is the remaining check before merge.

Exchange dependencies

/v1/retrieve/quote, x-exchange-max-credits, and /v1/skills/resolve are on Exchange codex/contextual-provider-tools; /v1/provider-terms/requirements is in the Exchange provider-terms worktree (uncommitted there as of 2026-09-11). Neither is on Exchange main. /v1/usage-events/billing is on main and needs a non-blank EXCHANGE_INTERNAL_SECRET.


Summary by cubic

Adds Alexandria provider tools to Search and Scrape: the alexandria source / domainTools on Search, and domainTools on URL scrape, attach free tool contracts to data.tools, while Scrape's alexandria body executes 1–10 paid provider calls and returns data.alexandria. All are gated by the exchangeRetrieve flag; domainTools without access (or under ZDR) returns 403 on both surfaces, and the legacy /exchange/retrieve route shares the Scrape handler. The previously used exchange body key is rejected as unrecognized.

Billing and idempotency

  • Provider execution runs inline on existing billing rails; no new queue, worker, or migration.
  • The charge id is sha256(teamId, x-request-id), so send x-request-id on every paid request or a retry is a new charge.
  • A Redis record keyed by that id replays completed requests without re-executing, answers 409 for a different payload or in-flight request, and holds uncertain outcomes as 503 without auto-retry.
  • Pre-execution refusals release the record so the same id can retry.
  • Paid requests fail closed when authorization, quote, reservation, or USE_DB_AUTHENTICATION is unavailable.
  • If the ledger enqueue fails after retries, Exchange usage stays pending for reconciliation; the direct-Autumn charge is refunded and firebill charges stand.

Rollout

  • Requires Exchange routes only in the codex/contextual-provider-tools and provider-terms worktrees, not on main.
  • Set EXCHANGE_INTERNAL_SECRET (non-blank) for usage-event billing reports.
  • Paid settlement is mock-tested only; run one call against the Autumn sandbox before merging.

Written for commit 47ef264. Summary will update on new commits.

Review in cubic

Build from main using Exchange tool contracts, Autumn credit holds, and one BullMQ execution job. Preserve team-scoped retries, provider agreements, budgets, and fail-closed handling of uncertain paid outcomes without a new database migration or billing batch ledger.
…billing rails

Replace the dedicated BullMQ queue, QueueEvents wait, eight-phase state
machine, worker-side semaphore and index-worker consumer with an inline
request path guarded by a Redis idempotency record keyed by team and
x-request-id (the charge id). The record is written before the provider call,
so a crash is found as an unresolved request rather than re-executed; a
completed request replays its response; a different payload under the same id
answers 409 duplicate_request; a running one answers 409 request_in_flight.

Billing now follows the monitor runner and scrape worker: lockCredits with a
deterministic lock id, finalizeCreditsLock for the actual receipt (through the
service, which already routes firebill settles), the ledger write through the
existing billing queue with a deterministic job id instead of a direct
bill_team_7 call, and a fire-and-forget usage report to Exchange with the
existing billing-report retry semantics. Pre-execution refusals (authorization,
quote, hold denied or skipped, Exchange 4xx) release the record so the same id
can retry; only an outcome the Exchange may have executed is held for
reconciliation.

Provider authorization reads the Exchange catalog terms and the organization's
access flags, the same gate the URL-routed Exchange scrape uses, instead of a
raw query against an undeclared table and a terms digest nothing writes. The
Exchange side has no /v1/provider-terms/requirements route.

Controller: relay a failed single legacy call with the Exchange's status and
code instead of success:true; accept boostConcurrency in __agentInterop and
prefer its request id; log one requests row per execution with the
External-Request-Id attribution; let wrap() format Zod errors; drop the
redundant endpoint-restriction check.

Search: one tools-only predicate shared by the credit middleware and the
keyless projection (developer category still bills); www-normalised domain
matching; combined tools capped at limit; developer result URLs included and
image URLs excluded from domain matching; dead status/level fields removed.

Tests cover replay without re-execution, duplicate-payload 409, denied and
skipped holds releasing the claim, transient quote failures not cached,
ambiguous Exchange outcomes held without settling or re-executing, definitive
refusals releasing the hold, over-budget receipts, an unsettled confirm
returning the answer without a ledger write, non-billable runs, in-flight
versus abandoned records, and the legacy single-call error relay.
…findings

- Authorize through Exchange /v1/provider-terms/requirements, which answers 404
  for any unknown or hidden provider, so an unlisted provider never reaches the
  quote or /v1/retrieve. Agreements compare against the organization flags the
  URL-routed Exchange path already uses. Drops the /v1/providers catalog
  lookup, which lists only URL-routed providers, and restores lib/exchange.ts
  apart from the shared billing-report transport.
- Reject a blank EXCHANGE_INTERNAL_SECRET, matching FIREBILL_SECRET.
- Bound relayed provider error statuses (schema 100-999, controller 400-599,
  else 502) so res.status can never throw.
- Log a requests row only when a provider execution was attempted, and keep
  the original scrape_id on replays.
- Mark the ledger enqueue refundable only on the direct Autumn route; a
  firebill-routed charge stands pending reconciliation.
- Live smoke (17 checks) passes against the local Exchange snapshot.
@developersdigest
developersdigest marked this pull request as ready for review September 12, 2026 02:06

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 20 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread apps/api/src/services/alexandria/access.ts Outdated
Comment thread apps/api/src/search/alexandria.ts
Comment thread apps/api/src/services/alexandria/retrieve.ts Outdated
Comment thread apps/api/src/controllers/v2/scrape-alexandria.ts
Comment thread apps/api/src/services/alexandria/retrieve.test.ts
Comment thread apps/api/src/services/alexandria/retrieve.test.ts Outdated
Comment thread apps/api/src/services/alexandria/README.md Outdated
Comment thread apps/api/src/services/alexandria/README.md
…queue failure, doc fixes

- Compare the set of providers the Exchange answered for against the set
  requested, so duplicate entries cannot mask a missing provider.
- When the ledger enqueue fails after retries on the direct Autumn route,
  refund the confirmed charge (same compensation as the scrape worker); on
  the firebill route the durable charge still stands pending reconciliation.
- README: forced-ZDR refusal happens before any record exists; a release that
  does not land leaves an expiring hold, so a retry may briefly hold twice.
- Tests: one table case per fail-closed refusal (authorization, quote
  outage, malformed quote, billing unconfigured, denied and skipped holds)
  and the enqueue-failure refund.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/services/alexandria/retrieve.ts
Comment thread apps/api/src/services/alexandria/retrieve.ts
Comment thread apps/api/src/services/alexandria/retrieve.ts Outdated
`domainTools: true` (default off) on an ordinary /v2/scrape attaches
`data.tools` matched to the scraped page's domain, in the same shape Search
returns, for teams with Exchange access and outside ZDR. Discovery is free
and never fails the scrape. Also drops the remaining inline comments in the
Alexandria files.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/controllers/v2/scrape.ts Outdated
…ils; 403 for domainTools without access

- Decide the refund route once, before the credit hold, and carry it to the
  ledger write instead of looking it up again after settlement.
- recordLedgerUsage reports whether the enqueue landed; on failure the
  Exchange usage is left pending for reconciliation instead of confirmed,
  and the direct-Autumn charge is refunded (refundCredits logs its own
  failures; there is nothing further to catch).
- URL scrape with domainTools now returns the same 403 Search does for a team
  without exchangeRetrieve or under ZDR, instead of silently omitting tools.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/services/alexandria/retrieve.ts
…alexandria

POST /v2/scrape now takes `alexandria` (one call or 1..10) and returns
`data.alexandria`; the old `exchange` key is an unrecognized-key 400. Search
and scrape 403 messages say "alexandria source". Legacy /exchange/retrieve
is unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 6 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant