Inspiration
A technical B2B supplier gets work through three doors: public tenders, inbound RFPs and RFQs, and design requests from customers. Each one arrives as a pack of specifications, forms and clarifications that can run to hundreds of pages. Before anyone can write a word, someone has to read the whole pack, list every requirement, check the hard gates (turnover, insurance, certificates, references), decide which lots are worth bidding for, and find the company's evidence for each answer. We wanted one engine that does that reading and checking, shows its sources, and leaves the decision to a person.
What it does
Pactovio is a pre-sales engine for technical B2B suppliers. Three doors in, one engine, outputs a bid team can use.
- Doors: a nightly scan of public tenders (TED, UK Find a Tender, eTenders), inbound RFPs and RFQs by upload, paste or mailbox (a request with no attachments is read from its message text), and design requests from customers.
- Engine: reads the whole pack, extracts every requirement with its document and page, decides bid or no-bid per lot, matches requirements to the company's evidence, drafts answers and a priced quote, checks every claim, and asks a person to approve.
- Outputs: the buyer's own response form filled in (DOCX), a requirement matrix with sources, per-lot verdicts, a priced quote (DOCX and XLSX), events to the team's tools, and a 24x7 agent you can chat with or reach over MCP.
The app has these screens: Today, Inbox, Tenders (per-lot verdicts citing document and page, every gate, the requirement matrix, drafted answers with struck claims and a NeMo Guardrails panel, the filled form, every run step with model, tokens and cost, buyer intel), Quotes (design requests matched against datasheets as MET / PARTIAL / GAP with citations), Agent, Connections (15 connectors with live status), Evidence, Billing & usage, How it works, Pricing and Help.
Decisions are mostly code, not model. Gates are checked in code against the company profile as of the submission deadline. Requirement status (MET, PARTIAL, GAP, NEEDS_EVIDENCE) comes from similarity thresholds and explicit rules. Per-lot BID, NO_BID or CONDITIONAL is decided by code; the model only writes the reasoning. Every sentence of a drafted answer is split into claims (certifications, SLAs, headcounts, client names, money, dates), each is checked against the evidence in code, and unsupported ones are struck and tagged before a person sees them.
The agent asks before it acts. State-changing tools never run from the model's call. They return a pending card, and a person approves in chat or from a signed link.
How we built it
NVIDIA models, all on Nebius Token Factory:
| Model id | Job |
|---|---|
nvidia/Nemotron-3-Ultra-550b-a55b |
Whole-pack read, requirement extraction (section by section), per-lot verdict reasoning. The pack text goes first in every prompt of a run so later calls hit the prompt cache. Also the agent's last fallback. |
nvidia/Nemotron-3.5-Super-VL |
The agent's tool loop (chat and MCP), drawings and scanned pages, evidence scans, and the judge for NeMo's self check facts (the job falls back to nemotron-3-super-120b-a12b if Super VL is not listed). |
nvidia/Nemotron-3_5-Lightning |
Nightly notice triage (flex tier), answer drafting, email classification, buyer-intel summaries. Thinking off. |
nvidia/nemotron-3-super-120b-a12b |
First fallback for the agent if Super VL is not served. |
Qwen/Qwen3-Embedding-8B |
Evidence and requirement embeddings, ranked by cosine in code (no reranker). Datasheet rows use 256-dim Matryoshka vectors. |
Vision falls back to google/gemma-3-27b-it if Super VL is not served.
Nebius Token Factory features: one OpenAI-compatible client for all of the above; json_schema structured output generated from zod, validated, with one retry carrying the validation error; prompt caching (pack first in every prompt); the flex tier for the overnight scan; thinking switched off where speed matters. Every model call is a row in run_steps with model, purpose, input, output and cached tokens, time and cost, and the product shows it on each run. Costs use our own price table, which is an estimate, not the Nebius price list.
Nebius AI Cloud Serverless Jobs and NeMo Guardrails: drafted answers are audited by NeMo Guardrails 0.24.1 running as a Serverless Job (python:3.12-slim, cpu-d3 2 vCPU / 8 GB, eu-west2). The audit has three layers, and result_json labels which verdict came from NeMo and which from our code:
- NeMo's regex output rail, with our patterns for five claim types (certifications, SLAs, headcounts, client names, money).
- Our own Colang flow
claim auditwith a custom action: every concrete value in a flagged claim (for example ISO 27001, 64 staff, EUR 9.8M) must appear in that answer's evidence. - NeMo's
self check factsrail with Nemotron 3.5 Super VL as judge and the answer's evidence as the relevant chunks.
A claim is supported only if our check and NeMo's self check agree. A 10-prompt red team (fabricated certificate, inflated headcount, invented SLA, invented client, inflated turnover, wrong certificate number, prompt injection inside the answer, authority spoof, uncited price commitment, mixed true and false) runs in the same job. The job is started by hand from the Nebius CLI (scripts/start-audit-job.sh), reads the queued audits from the app, posts results back to /api/ingest/audit, and exits. There is no schedule, and no Worker calls the Nebius control plane.
Fine-tune (measured, not served): a LoRA fine-tune of nvidia/Llama-3.1-Nemotron-Nano-4B-v1.1 (NVIDIA Open Model License) turns a tender notice's text into five fields: deadline, lot count, CPV division, whether an estimated value is stated, and buyer country. Data: 2,188 EU TED contract notices pulled by the job from TED's public API (reusable under the Commission's reuse policy; labels from TED's structured fields; no model-written text), split by buyer into 1,868 / 100 / 220 with 0 overlap. Training ran in one Nebius Serverless Job on 1x H100 in eu-north1 (LoRA r=16, 2 epochs, 315 s; job aijob-e00eje0fdntak0kj5a, deleted after completion; estimated $0.5-0.7, billing not read). Code: jobs/finetune/, results in jobs/finetune/results.json.
Tavily: buyer intel calls Tavily Search and then Tavily Extract on the public buyer's official site and stores cited facts. Results are cached in Cloudflare KV and capped per session and per day; read-only views never call Tavily.
Cloudflare stack: one Worker, pactovio-api (Hono with zod OpenAPI), serving /api, /v1, /mcp, /webhooks and /ics. Cloudflare Workflows run the pack read, the scan and intake (PackRunWorkflow, ScanWorkflow, IntakeWorkflow). The AgentHub Durable Object holds the 24x7 agent (SQLite storage, WebSocket hibernation, alarms for the morning digest and deadline reminders). A Queue fans events out to webhooks and connected apps. D1 holds tenders, the matrix, verdicts, evidence and the ledger; R2 holds pack files; KV is the cache. A cron at 06:00 MYT starts the nightly scan. The front end is a Cloudflare Pages project in plain HTML, CSS and ES modules with no build step; its _worker.js routes API paths to the Worker over a service binding.
Around the engine: 15 connectors through Composio managed auth (Gmail, Outlook, Slack, Microsoft Teams, HubSpot, Salesforce, Google Drive, Dropbox, Google Calendar, Outlook Calendar, Jira, Asana, DocuSign, QuickBooks, Xero). The agent's 17 tools are also served over MCP at /mcp. Billing is Dodo Payments in test mode. An optional Mac desk agent runs Hermes Agent 0.15.1 inside an NVIDIA OpenShell sandbox with a default-deny network policy that allows only Token Factory, the Pactovio host and Slack.
Measured
All figures are from runs on 30 Oct 2026. Costs are estimates from our price table.
| What | Result |
|---|---|
| Fresh pack read, Met Eireann coastal visibility sensors pack (107 pages), production | 2.64M input tokens (2.26M cached), 100K output, 2 m 49 s, about $0.73 |
Live pack read, Teagasc pack (205 pages, 3 lots), run run_01m65njp7dcgn4akm2eb38a60d |
4.68M input tokens, 187K output, 3 min 51 s, about $1.23; about 100k of each 108k-token Ultra prompt served from cache |
Fresh pack read for the demo workspace, Met Eireann pack, run run_01m666zkm8bfepyakwpd6q0a52 |
5.34M input tokens, 94K output, about $1.23; verdict No bid 34/100 on both lots (turnover gate: about EUR 0.9M against EUR 1.5M required) |
Text-only RFQ pasted into the inbox (no attachments), run run_01m66mfbp3r0trz5eeda2hxn4b |
Classified RFP / RFQ at 95%; the email body became the pack; verdict Bid with conditions 35/100, reasons citing message.txt p.1 |
NeMo Guardrails audit job pactovio-audit-20261030-065324 on Nebius, against production |
About 5 minutes including image pull and pip install; 5 answers of the Met Eireann pack audited; 1 claim flagged by the regex rail, 1 unsupported by both our check and NeMo's self check; red team 10 of 10 caught |
| LoRA fine-tune, Nemotron Nano 4B, 220 held-out TED notices (base zero-shot → tuned) | All five fields exact 3.6% → 55.0%; deadline 95.0% → 100%; lot count 92.7% → 100%; value stated F1 69.8% → 100%; CPV division 12.3% → 58.2%; buyer country 29.1% → 94.6%. Most of the base gap is format-following |
| Serverless Job spike (start, run, exit) | Create to COMPLETED about 3 min 15 s |
| Connectors, live against real connected test accounts | 9 checks passed across 8 connectors, 0 failed |
| Gmail real-time intake | Email in, Composio trigger, Lightning classified it as design, intake created: 24 s end to end (25 s on a re-test after a routing fix that same evening) |
On the real Teagasc tender (public, 3 lots, EUR 5.95M), the sample company's insurance cover (EUR 6.0M) is below the EUR 6.5M the pack requires, so all three lots come out CONDITIONAL rather than BID. That is the gate working as intended.
Event rules also fired on their own in production with no one clicking: a finished pack read opened gap tasks in Jira and Asana, a drafted quote was uploaded to Google Drive and Dropbox, and the agent posted "Quote ready" and both lot verdicts to Slack. A no-bid verdict correctly pushed nothing to the CRM.
Challenges we ran into
- Truncated JSON. Very long structured answers from Ultra were sometimes cut off mid-JSON, leaving whole lots empty. We page the extraction by lot and section and salvage partial JSON.
- A model went missing. For several minutes on 30 Oct,
nvidia/Nemotron-3.5-Super-VLreturned 404 and was missing from/v1/models. We added the fallback chain tonemotron-3-super-120b-a12b, then Ultra, andgemma-3-27b-itfor vision; the NeMo audit job checks/v1/modelsat start and judges withnemotron-3-super-120b-a12bif Super VL is not listed. - Slow embeddings.
Qwen3-Embedding-8Btook about 25 s per request and 34 s for a 304-text batch, and full 4096-dimension responses took minutes, so we use 1024- and 256-dimension outputs. - Starting jobs from the backend. The Nebius AI Cloud control plane (IAM token exchange and the Jobs API) timed out from Cloudflare Workers with HTTP 522, so the audit job is started from the CLI.
- Attributing NeMo verdicts. NeMo 0.24 runs every configured output flow in one pass, so the audit builds one
LLMRailsper rail to tell which rail said what. - Test accounts. Microsoft account creation and the Intuit sandbox sign-up were refused, so those connectors are covered by automated tests but not yet tested against live accounts.
What is real and what is sample data
- Real: the tenders and their documents are real public notices from eTenders, TED and Find a Tender. Every model call, the NeMo audit job, the Tavily lookups on public buyers' sites, and the connector checks against real connected test accounts are real.
- Sample: Harbour Digital Ltd (Cork) and Kestrel Sensing Ltd (Galway) are fictional and labelled "Sample company" in the app, along with their evidence and the Kestrel catalogue. The default demo workspace for an electronics manufacturer is built only from that company's public website and records; it is a demo, not a customer.
- Billing: Dodo Payments test mode only. No real payment is possible.
Limits
- 8 of the 15 connectors are live-tested. HubSpot, Outlook, Outlook Calendar, Microsoft Teams, QuickBooks, DocuSign and Xero are built and covered by automated tests, not yet tested against live accounts.
- Scanned PDFs without a text layer are rejected on evidence upload (upload PNG or JPG instead), and PDF image pages are not rendered for the vision step.
- Form fill covers table rows keyed by reference number; free-form content controls are left untouched.
- Embeddings are slow (about 25 s a request), which shows on evidence upload.
- All judges on a sample workspace share its agent inbox; chats are private per session.
- The NeMo audit job is started by hand from the CLI, not on a schedule.
- One fresh pack run is in flight at a time across the whole demo, to stay inside the Token Factory quota (1,000 RPM, 2M TPM). Tavily is capped at 25 live calls a day and 150 a month.
- The Mac desk agent answers from the production API with its own key; its own Slack gateway has not been tried live (the hosted agent posts to Slack instead).
What's next
- Live-test the seven remaining connectors.
- Render PDF image pages for the vision step, so scanned packs and scanned evidence can be read.
- Extend form fill to free-form content controls.
- Give each visitor their own agent inbox.
- Start the audit job from the backend once the Jobs API can be reached from Workers.
- Serve the fine-tuned notice extractor for the nightly scan's triage, and extend it to requirement extraction.
Try it
Open https://pactovio.pages.dev/app. No sign-in and no code are needed: every visitor gets a capped judge session on the demo workspace, and the two fictional sample companies are one switch away. Per session you get 2 fresh pack runs, 3 Tavily buyer refreshes and 10 agent messages a minute. Billing is test mode.
Built With
- cloudflare-d1
- cloudflare-durable-objects
- cloudflare-kv
- cloudflare-pages
- cloudflare-queues
- cloudflare-r2
- cloudflare-workers
- cloudflare-workflows
- composio
- dodo-payments
- hermes-agent
- hugging-face-transformers
- lora
- nebius-ai-cloud
- nebius-serverless-jobs
- nebius-token-factory
- nemo-guardrails
- nvidia-nemotron
- nvidia-openshell
- python
- qwen3-embedding
- tavily
- typescript

Log in or sign up for Devpost to join the conversation.