AI-Powered Public Tender Discovery & Bid Management Platform
Bandifinder.it helps Italian businesses discover, analyze, and bid on public tenders across Italy and the EU. It combines a deterministic, explainable scoring engine with multi-agent AI and structured data pipelines β from discovery to post-mortem analytics.
Every score is shown as the sum of its parts. Each segment of the meter is as wide as that component is worth (CPV 25, certifications 20, economic fit 20, geography 15, experience 10, deadline 10), and filled to what was earned β so the gaps show exactly where the points went, sized by how much they mattered.
- Natural language search via AI chat agent
- TED API v3 + ANAC integration, ingested every 6h via Supabase
pg_cron - Filters (country, period, value range), full-text search, paginated results
- Semantic vector search (Pinecone) + GraphRAG hybrid retrieval
- Country filter defaults to Italy; widening to the EU is one click
- 6 weighted components out of 100: CPV match (25), certifications (20), economic fit (20), geography (15), experience (10), deadline feasibility (10)
- Deterministic β no LLM involved, so every score is reproducible and defensible
- Per-component explanations in Italian, plus a pass/fail eligibility checklist
- BID (β₯70) / REVIEW (40β69) / SKIP (<40) recommendations
- Scores cached per-org with a 24h TTL
- The same engine backs the REST API and the chat agent, so the two can never disagree
- Visitors browse tenders and see real fit scores without creating an account
- A short inline form (sector, regions, certifications, revenue) feeds the scoring engine
- The profile lives in
localStorageand travels as a request header; nothing is persisted server-side - A guest carries no user id and no organization, so guest requests can never reach another org's data
- Saving bids, favourites and exports prompt for signup
- 6 stages: Nuovi β In Revisione β In Preparazione β Inviati β Vinti / Persi
- Drag-and-drop (dnd-kit) + hover move buttons
- Checklist items, threaded comments, team assignments
- Auto-generated compliance checklist on bid creation
Pipeline stages darken along a single teal ramp, so how far a bid has travelled is legible from colour alone. Only the terminal states break it.
- Win/loss tracking with competitive intelligence
- Win rate segmented by value range and buyer
- Competitor analysis (direct bid outcomes + TED award data)
- Lessons learned feed
The four headline figures are one funnel β analysed, submitted, won, average value β so they read in order. Missed deadlines is a different kind of number and gets its own card, turning vermilion the moment it is non-zero.
- Modular connector system (TED API v3, ANAC CSV, TED Awards)
- Deduplication with amendment detection (8-field comparison)
- Automatic notification generation for saved searches
- Scheduled by Supabase
pg_cron; admin manual trigger available
- Stripe integration with 4 tiers (Free / Starter / Pro / Enterprise)
- Daily search quotas and rate limits held in Postgres, so they are shared across all serverless instances and survive restarts
- Quota consumption is a single atomic statement β concurrent requests cannot overshoot a limit
- Checkout, customer portal, webhook-driven plan sync
- Clerk authentication (JWT, organizations, RBAC), verified server-side on every protected route
- GDPR cookie consent banner (controls Sentry replay + Vercel Analytics)
- Sentry error tracking (frontend + API)
- Guided onboarding flow (org creation β profile completion)
- Full Italian UI
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Next.js 16 Frontend (Vercel) β
β React 19 Β· Clerk Auth Β· ShadCN UI Β· TanStack Query β
β dnd-kit Β· Sentry Β· Vercel Analytics β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β REST + SSE
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β Hono.js API (Vercel Serverless) β
β LangGraph Supervisor β 5 Specialized Agents β
β Scoring Engine Β· Ingestion Pipeline Β· Stripe β
β Security Middleware (auth, rate limit, PII) β
β Observability (metrics, logging, tracing, Sentry) β
βββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββββββ
β β β β
Supabase Pinecone TED API Stripe
Postgres Vectors + ANAC Billing
| Route group | Access |
|---|---|
/tenders/*, /analytics/*, /compare/* |
Signed-in or guest |
/agent/*, /suggestions/* |
Optional auth (public chat) |
/company, /bids, /billing, /preferences, /notifications, /saved-searches, /organizations, /export |
Verified Clerk session required |
/ingestion/* |
Admin session or Authorization: Bearer $CRON_SECRET |
/webhooks/* |
Signature-verified (Clerk via Svix, Stripe) |
Identity comes only from a verified Clerk JWT. Guests get a separate context that carries no user id and no organization id.
A router pattern: classify intent, route to one specialist, format the result.
| Agent | Role |
|---|---|
| Supervisor | Keyword intent classification and routing |
| Search | TED API queries and tender discovery |
| Analysis | Eligibility and scoring β delegates to the scoring engine |
| Ranking | Ordering and shortlisting β delegates to the scoring engine |
| Personalization | Profile-based recommendations |
| Contract Review | Risk analysis and clause extraction |
| General | Product questions and greetings (no tools) |
Agent identity comes from the graph config (configurable.user_id), never from
the model. Conversation threads are checkpointed to Postgres when DATABASE_URL
is set, falling back to in-memory otherwise.
- Node.js 22+ (the deployed API targets Node 24)
- npm
npm install # frontend
cd backend && npm install # APIRoot .env / .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_TENDER_API_BASE=http://localhost:3001 # API base URL
NEXT_PUBLIC_SENTRY_DSN=https://...
SENTRY_AUTH_TOKEN=sntrys_...
SENTRY_ORG=your-org
SENTRY_PROJECT=your-projectbackend/.env.local:
OPENROUTER_API_KEY=sk-or-v1-...
OPENAI_API_KEY=sk-... # embeddings for Pinecone
PINECONE_API_KEY=pcsk_...
SUPABASE_URL=https://....supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
DATABASE_URL=postgresql://... # Postgres connection string (use the pooler)
CLERK_SECRET_KEY=sk_...
CLERK_WEBHOOK_SECRET=whsec_...
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_PRO=price_...
STRIPE_PRICE_ENTERPRISE=price_...
FRONTEND_URL=http://localhost:3000
SENTRY_DSN=https://...
CRON_SECRET=... # shared secret for scheduled ingestionOptional, local development only:
| Variable | Purpose |
|---|---|
PORT |
API port (default 3001) |
CORS_EXTRA_ORIGINS |
Comma-separated extra origins, e.g. when port 3000 is taken |
ALLOW_INSECURE_HEADER_AUTH |
Set to true to accept an unverified x-user-id header. Never set this in a deployed environment β it is a complete authentication bypass |
LOG_LEVEL |
debug | info | warn | error |
DATABASE_URLis a Postgres connection string, notSUPABASE_URL(which is the REST endpoint). Use Supabase's pooler URL β direct connections exhaust Postgres connection limits from serverless.
Apply the migrations in supabase/migrations/ in order. Then schedule the
housekeeping job:
select cron.schedule('purge-usage', '17 3 * * *', 'select purge_usage_data()');# Terminal 1 β Frontend (http://localhost:3000)
npm run dev
# Terminal 2 β API (http://localhost:3001)
cd backend && npm run devcd backend && npm test262 tests covering the scoring engine, auth enforcement, guest access, usage limits, agent tools, and GraphRAG isolation. The SQL in migration 006 is executed against a real Postgres via PGlite rather than mocked.
Tender-Finder-AI/
βββ app/ # Next.js App Router
β βββ page.tsx # AI Chat interface
β βββ dashboard/ # KPIs, high-fit tenders, deadlines
β βββ tenders/ # Search, filters, table
β β βββ [id]/ # Detail, scoring, eligibility
β βββ pipeline/ # Kanban board (dnd-kit)
β βββ bids/[id]/ # Bid detail, checklist, comments
β βββ analytics/ # Win/loss, competitors, lessons
β βββ onboarding/ # Org creation + profile wizard
β βββ settings/ # Profile, notifications, billing, team
β βββ admin/ # Platform metrics + ingestion control
βββ components/
β βββ ui/ # ShadCN primitives
β βββ tender/ # ScoreMeter, FitScoreBadge, DeadlineCountdown
β βββ guest/ # GuestBanner, GuestProfileDialog
β βββ notifications/ # Bell, drawer, items
β βββ onboarding/ # Stepper, completeness bar
β βββ Header.tsx # Navigation
β βββ OnboardingGate.tsx # Global onboarding enforcement
β βββ CookieConsent.tsx # GDPR banner
βββ lib/
β βββ hooks/ # useApiQuery, useBids, useBilling, etc.
β βββ utils/ # Formatters (EUR, dates, urgency)
β βββ guestSession.ts # Guest id + profile (localStorage)
β βββ cookieConsent.ts # Consent utility
βββ backend/src/ # Hono.js API
β βββ app.ts # Route mounting + middleware
β βββ server.ts # Local dev server
β βββ loadEnv.ts # Env loading, imported before the app
β βββ agents/ # LangGraph supervisor + agents + tools
β βββ routes/ # tenders, bids, billing, analytics, β¦
β βββ lib/
β β βββ db/ # Supabase query layers
β β βββ scoring/ # 6-component engine + compliance
β β βββ ingestion/ # TED, ANAC, Award connectors + runner
β β βββ graphrag/ # Knowledge graph + hybrid retrieval
β β βββ guest.ts # Guest profile parsing + scoring adapter
β β βββ scoringContext.ts # Resolves the profile to score against
β β βββ checkpointer.ts # LangGraph Postgres checkpointer
β β βββ observability/ # Metrics, logging, tracing
β βββ middleware/ # auth, guest, rate limit, billing, PII
β βββ __tests__/ # Vitest suites (incl. PGlite SQL tests)
β βββ build.mjs # esbuild β Vercel Build Output API
βββ supabase/migrations/
β βββ 001_initial_schema.sql # Core tables
β βββ 002_tender_scores.sql # Score cache
β βββ 003_bid_pipeline.sql # Bids, assignments, comments, checklist
β βββ 004_bid_outcomes.sql # Post-mortem outcomes
β βββ 005_tender_awards.sql # Award data fields
β βββ 006_usage_limits.sql # Quota + rate limit counters
βββ vercel.json # Rewrites + CORS headers
The interface is built around procurement as a precision instrument.
- Type β Archivo (display), IBM Plex Sans (interface), IBM Plex Mono (CPV codes, protocol numbers, figures)
- Palette β warm paper ground, deep teal primary (
Verderame), brass for caution, graphite for inert - One rule β vermilion means "time is running out" and nothing else. A poor fit is graphite, never red, which is what makes an approaching deadline actually register
- Score meter β segment widths are proportional to each component's maximum, so the bar shows both what you scored and how much each dimension counts
| Category | Technology |
|---|---|
| Frontend | Next.js 16, React 19, Tailwind CSS 4, TypeScript 6 |
| UI | ShadCN UI, Radix UI |
| State | TanStack Query, dnd-kit |
| Auth | Clerk v7 (JWT, orgs, RBAC) |
| Backend | Hono.js on Vercel Serverless, TypeScript 7 |
| AI | LangGraph 1.4, LangChain, OpenRouter |
| Database | Supabase Postgres |
| Vectors | Pinecone |
| Billing | Stripe |
| Testing | Vitest, PGlite |
| Monitoring | Sentry, Vercel Analytics |
| Deployment | Vercel |
| Method | Endpoint | Description | Guest |
|---|---|---|---|
| GET | /tenders |
List tenders | β |
| GET | /tenders/search |
Search with filters + fit scores | β |
| GET | /tenders/best |
Best matches for the profile | β |
| GET | /tenders/:id |
Tender detail | β |
| GET | /tenders/:id/analysis |
Scoring + eligibility | β |
| POST | /tenders/semantic |
Vector search | β |
| POST | /tenders/graphrag |
Hybrid graph retrieval | β |
| POST | /tenders/favorite |
Toggle favorite | β |
| Method | Endpoint | Description |
|---|---|---|
| GET | /bids |
List bids (with status filter) |
| POST | /bids |
Create bid from tender |
| PATCH | /bids/:id |
Update bid (status, priority) |
| GET/POST | /bids/:id/outcome |
Record bid outcome |
| GET/POST | /bids/:id/comments |
Threaded comments |
| GET/PATCH | /bids/:id/checklist |
Compliance checklist |
| Method | Endpoint | Description |
|---|---|---|
| GET | /analytics/outcomes |
Win/loss stats, segments, competitors |
| GET | /analytics/kpis |
Dashboard KPIs |
| GET | /analytics/market-competitors |
TED award-based competitors |
| Method | Endpoint | Description |
|---|---|---|
| GET | /billing |
Current plan |
| GET | /billing/plans |
Available plans |
| POST | /billing/checkout |
Stripe checkout session |
| POST | /billing/portal |
Stripe customer portal |
| Method | Endpoint | Description |
|---|---|---|
| POST | /agent/stream |
AI chat (SSE) |
| GET/POST | /company |
Company profile |
| POST | /ingestion/run |
Trigger ingestion (admin or cron secret) |
| POST | /webhooks/clerk |
Clerk webhook |
| POST | /webhooks/stripe |
Stripe webhook |
| GET | /health Β· /metrics |
Health and metrics |
Both projects deploy to Vercel from this repository, and both watch main β
a single push triggers two production deployments.
| Project | Root | Notes |
|---|---|---|
| Frontend | repo root | Next.js preset |
| API | backend/ |
framework: null β build.mjs emits a Build Output API bundle, so no framework preset should build on top of it |
Before deploying the API, confirm:
CRON_SECRETis set, and thepg_croningestion jobs send it asAuthorization: Bearer β¦DATABASE_URLis set to the Postgres pooler URL- Migrations through
006are applied CLERK_SECRET_KEYandSUPABASE_URLexist in Preview as well as Production, or preview deployments will reject every authenticated request
MIT License β see LICENSE.
Built for the Italian and EU public procurement community




