eutenders
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@eutendersShow me recent software tenders and their renewal forecasts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
licita-agent
Agent-native procurement intelligence API for the Spanish public sector (IT / software / cybersecurity vertical, CPV 72*/48*), built from TED (Tenders Electronic Daily) award notices.
An external AI agent can — with no human operator and no prior knowledge —
discover the service (GET /llms.txt), understand it (/openapi.json,
/docs), read the price ladder (GET /v1/pricing), pay per call
(x402-compatible 402 flow with a dev faucet), and consume data over REST
or MCP (8 tools at /mcp). Every data row carries provenance
(source + source_ref + TED URL). Nulls are never fabricated.
Questions it answers: who bought, who won, for how much, under which CPV codes, when contracts start/end, which active tenders look similar to a company's track record, and which contracts are likely to be re-tendered soon.
Live instance
A public production instance is live — agents can connect right now:
MCP (streamable-HTTP):
https://eutenders.duckdns.org/mcpDiscovery:
https://eutenders.duckdns.org/llms.txt·/openapi.json·/v1/pricing·/docsPayments: x402 v2 (USDC on Base), pay-per-call, free discovery calls. Unpaid tool calls return
{"payment_required": true, ...}withhow_to_paysteps; create the payment with an x402 client from thePAYMENT-REQUIREDrequirement and retry withpayment_token.
Client config snippet for the live instance:
{
"mcpServers": {
"eutenders": { "type": "streamable-http", "url": "https://eutenders.duckdns.org/mcp" }
}
}Related MCP server: secop-mcp-server
Distribution & contact
Licita is listed in MCP registries and directories for agent and human discovery:
MCP registry manifest:
server.json— streamable-HTTP athttps://eutenders.duckdns.org/mcp.Static server card:
/.well-known/mcp/server-card.json(identity, SSE URL, all 11 tool schemas).Glama: glama.json + Glama server (badge).
mcp.so: Licita on mcp.so.
x402 Bazaar: paid 402s carry
extensions.bazaar, so x402 facilitators catalog Licita automatically.
For a project, white-label data deal or prepaid plan, contact eutendersai@gmail.com.
Architecture
┌──────────────────────────── app (Fastify, src/index.ts) ───────────────────────────┐
│ │
TED Search API ──► │ ingestion REST /v1 payments MCP web │
v3 (no auth) │ src/ingest/ted.ts src/api/server.ts src/pay/* src/mcp/ src/ │
(poll, 24 mo) │ src/ingest/normalize src/api/routes/* middleware 402 → server.ts web/ │
PLACSP ATOM ─────► │ src/ingest/placsp.ts openapi, ratelimit X-PAYMENT verify 8 tools, pages │
(behind flag) │ normalize-placsp.ts │
│ src/ingest/scheduler openapi, ratelimit X-PAYMENT verify 8 tools, pages │
│ src/forecast/signals src/obs/* (logs, dev faucet + /mcp /docs │
│ (renewal signals) metrics, request replay protection /llms │
│ _log rows) │
└──────────────┬─────────────────────────────────────────────────────────────────────┘
│ pg (node-postgres, raw SQL)
▼
PostgreSQL 16+ (migrations/001_core.sql)Area | Files |
Wiring / config / DB |
|
Ingestion + scheduler |
|
Forecast signals |
|
REST API + observability |
|
Payments |
|
MCP |
|
Discovery surfaces |
|
Acceptance test |
|
Quickstart (Docker)
cp .env.example .env # edit secrets at minimum
docker compose up --build -d # db (postgres:16-alpine) + app (migrate → start)
docker compose logs -f appThe app container runs node dist/src/db/migrate.js && node dist/src/index.js on boot;
migrations are idempotent. The API is then at http://localhost:3000.
Without Docker (local dev)
Requires Node 20+ and a PostgreSQL 16+ — or use the bundled embedded Postgres:
npm install
npx tsx scripts/dev-db.ts & # embedded postgres on :5433, prints DATABASE_URL
export PGHOST=127.0.0.1 PGPORT=5433 PGUSER=licita PGPASSWORD=licita PGDATABASE=licita
npm run migrate # idempotent
npm run dev # tsx src/index.ts, listens on :3000Company identity (migration 003)
Companies are deduplicated per source by source_ref, and across sources ONLY
by exact normalized NIF (uppercase, no spaces/dots/dashes) — never by name.
company_identifiers (schemes nif/ted/placsp) is the cross-source
backbone; company_aliases records alternative observed names. Conflicting
late-NIF discoveries are logged as identity_conflict and never merged
automatically. For databases ingested before migration 003, run once:
npm run backfill-identity # idempotent; registers identifiers/aliases for existing rowsEnvironment variables
Secrets have no defaults. Boot fails fast with an error listing every
missing/invalid variable (src/config.validate.ts, called from src/index.ts).
Var | Default | Meaning |
|
| HTTP listen port |
|
|
|
| — | Admin Postgres URL; overrides the PG* parts. Used by migrations |
| — | Optional low-privilege role URL for app traffic (falls back to |
|
| Connection parts (compose sets these) |
|
|
|
|
|
|
|
| Max distinct client keys tracked by the in-memory rate limiter |
|
|
|
| required in dev mode | HMAC secret signing dev payment tokens |
| required always | Header |
|
| Harvest window (months back from today) |
|
|
|
|
| Hour (server-local) the scheduler fires the daily ingest |
|
|
|
|
| Max RFC 5005 feed pages fetched per PLACSP feed |
|
| Minimum delay between PLACSP feed requests (politeness) |
|
|
|
| facilitator/network defaulted; | Real x402 facilitator config (CAIP-2 network: |
First ingestion
Either trigger one harvest manually:
# docker (prod profile): docker compose -f docker-compose.prod.yml run --rm app \
# node dist/src/ingest/cli.js --once --source all # compiled CLI, no tsx in the image
npm run ingest -- --once # full window (INGEST_MONTHS), TED only
npm run ingest -- --once --max-notices 25 # small slice
npm run ingest -- --once --source placsp --max-notices 25 # PLACSP slice (needs PLACSP_ENABLED=true)
npm run ingest -- --once --source all # TED + PLACSPData sources
Source | Status | Notes |
TED Search API v3 | live (default) | ES award notices, CPV 72*/48*, |
PLACSP sindicación (CODICE 3.2 over ATOM) | live behind flag ( | Licitaciones feed (sindicacion_643) + contratos menores feed (sindicacion_1143), paged via RFC 5005 |
or let the app do it: INGEST_ON_BOOT=true arms the daily scheduler, which
fires the first harvest at the next INGEST_CRON_HOUR (04:00 UTC by default)
and re-harvests every 24h. It does not ingest at boot — the first harvest
is a manual --once run (see DEPLOYMENT.md).
Ingestion is idempotent (upserts keyed by (source_id, source_ref)) and safe
to kill/re-run. Verified live run against TED (2026-08-13, CPV 72*/48*, Spain,
last 24 months, capped at 25 notices):
notices_seen=25 → tenders=25, awards=44, companies=44, buyers=25,
contracts=44, forecast_signals=28, skipped=0, errors=0A full 24-month harvest sees ~8,000 award notices. After each run the
deterministic forecast signals (duration_expiry, framework_expiry,
recurrence) are deleted and recomputed.
Using it as an agent
Discovery order: /llms.txt → /openapi.json → /v1/pricing → paid calls
(or MCP at /mcp).
REST: the 402 → pay → retry flow
Two payment modes exist. x402 v2 (production contract): a paid endpoint
answered without payment returns HTTP 402 with a base64 PAYMENT-REQUIRED
response header — the exact USDC requirement. Sign an EIP-3009
transferWithAuthorization with an x402 client (or viem) and retry with that
payload in the PAYMENT-SIGNATURE header; the server verifies and settles
the payment through its facilitator before serving content. Proofs are
single-use (replay → 402).
dev (local only): mint a single-use HMAC token at the faucet and retry with
the legacy X-PAYMENT header:
BASE=http://localhost:3000
# 1. Paid endpoint without payment → 402 with x402-shaped body
curl -i "$BASE/v1/search?cpv=72&type=award"
# {"x402Version":1,"accepts":[{"scheme":"exact","network":"dev","asset":"USD",
# "amount":"0.02","payTo":"dev-faucet","resource":"GET /v1/search"}],
# "hint":"POST /v1/dev-faucet ...","error":{"code":"payment_required",...}}
# 2. Mint a single-use dev token (dev mode only; the faucet 404s in production)
TOKEN=$(curl -s -X POST "$BASE/v1/dev-faucet" \
-H 'content-type: application/json' \
-d '{"endpoint":"GET /v1/search"}' | jq -r .token)
# 3. Retry with the proof
curl -s "$BASE/v1/search?cpv=72&type=award" -H "X-PAYMENT: $TOKEN"
# {"data":[...],"meta":{"request_id":"...","price_usd":"0.02","paid":true,"provenance":[...]}}Endpoint keys for the faucet are the METHOD PATH strings from
GET /v1/pricing, e.g. "GET /v1/renewals". Tokens expire after 5 minutes
and are single-use (replay → 402 with reason replay).
The full autonomous flow is executable as the acceptance test:
BASE_URL=http://localhost:3000 npm run smoke # 11 steps; exits 0 only if all passRun the agent smoke test with real x402 payments
The smoke agent can run the actual x402 v2 flow — hitting the 402, signing EIP-3009 with the official x402 client and viem, and settling real testnet payments through the server's facilitator — instead of the dev faucet:
SMOKE_PAY_MODE=x402 \
SMOKE_WALLET_PRIVATE_KEY=0x<64 hex testnet throwaway> \
BASE_URL=http://localhost:3000 npm run smokeRequirements:
Server in x402 mode. Set
PAYMENTS_MODE=x402,X402_PAY_TO(the facilitator's recipient),X402_FACILITATOR_URL,X402_NETWORK(eip155:84532for Base Sepolia) andX402_HMAC_SECRETin.env.A Base Sepolia wallet. Generate a throwaway key (e.g.
openssl rand -hex 32). Never use a production key — every paid step spends real testnet USDC and the key lives in a local.envonly.Fund it (Base Sepolia): send a little ETH for gas plus USDC for the payments (~$0.44 per full run: search + tender + company + awards + buyer + renewals). Useful tools: the official Base faucet (
faucet.base.org), a USDC faucet (e.g. Circle's testnet faucet, which can airdrop USDC to Base Sepolia), and the SEP-24/USDC bridge helper of your choice. Verify withSMOKE_RPC_URL=https://sepolia.base.org(the smoke agent runs a best-effort pre-flight balance check and prints the wallet's USDC before the first paid step).Run. Missing
SMOKE_WALLET_PRIVATE_KEYexits with code 2 and a manual config message. A wallet with no USDC fails the first paid step with a clearinsufficient_fundsmessage instead of a cryptic 402.
If the facilitator rejects with insufficient_funds, fund the wallet and
re-run — every step mints a fresh proof, so there is no state to clean up.
MCP
Streamable-HTTP MCP endpoint at POST /mcp (stateless per request). Tools:
search_tenders, get_tender, get_company, get_company_awards,
get_company_opportunities, get_buyer_history, get_renewals,
get_pricing. Every tool accepts an optional payment_token argument; unpaid
paid-tools return {"payment_required": true, "price_usd": ..., "how_to_pay": ...}
as normal content (isError=false) — parse it, produce the payment payload
from the PAYMENT-REQUIRED requirement (or mint a dev faucet token locally),
retry with payment_token. payment_token is the same base64 payload a REST
client sends as PAYMENT-SIGNATURE.
Client config snippet:
{
"mcpServers": {
"licita": { "type": "streamable-http", "url": "http://localhost:3000/mcp" }
}
}Raw JSON-RPC example:
curl -s -X POST "$BASE/mcp" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search_tenders",
"arguments":{"cpv":"72","type":"award","payment_token":"<token>"}}}'Payments
PAYMENTS_MODE=dev(default). Priced endpoints return 402 in x402 shape. Proofs are HMAC-signed tokens (base64url(json).sig, secret =PAY_HMAC_SECRET) minted by the open dev faucetPOST /v1/dev-faucet. Verification checks signature, endpoint match, amount match, 5-minute expiry, and replay via a unique insert intopayments.proof(unique violation = replay). Every attempt is audit-logged. The faucet exists so agents can complete the flow without a human — disable it by leaving dev mode.PAYMENTS_MODE=x402(production). Real x402 v2 payments via the official@x402/core+@x402/evmpackages: priced endpoints return 402 with a base64PAYMENT-REQUIREDheader (v2) describing the exact USDC requirement (schemeexact, EIP-3009transferWithAuthorization, asset and amount in base units for the configured CAIP-2 network —eip155:84532Base Sepolia oreip155:8453Base mainnet). Clients pay and retry with the base64 payment payload inPAYMENT-SIGNATURE(v2) orX-PAYMENT(v1 legacy). The server verifies AND settles through the configured facilitator (X402_FACILITATOR_URL, defaulthttps://www.x402.org/facilitator) before serving content, fails closed (facilitator_unavailable) on facilitator errors, and records settled payments with payer address, tx hash and network in thepaymentstable (unique payload hash = replay protection). The dev faucet route is removed entirely in this mode (it 404s like any unknown path). No private keys are stored anywhere — settlement is gasless for the server; funds go straight toX402_PAY_TO.
Security model
Fail-fast configuration. No secret has a default. At boot,
validateConfigrequiresOPERATOR_KEYalways andPAY_HMAC_SECRETin dev mode; withNODE_ENV=productionit additionally requiresPAYMENTS_MODE=x402, a validX402_PAY_TO(0x+ 40 hex), an httpsX402_FACILITATOR_URL, a CAIP-2X402_NETWORK(eip155:84532Base Sepolia oreip155:8453Base mainnet; othereip155:<chainId>values are accepted as explicit overrides), and rejects known placeholder secrets (change-me,change-me-in-prod). All violations are reported in one error.Faucet gating.
POST /v1/dev-faucetexists only whenPAYMENTS_MODE=devANDNODE_ENV != 'production'; otherwise no route is registered at all (generic 404 — the faucet is undiscoverable).Rate limit: 60 req/min per client (in-memory token bucket; key = payment proof hash when present, else client IP). The bucket map is bounded (
RATE_LIMIT_MAX_KEYS, default 10,000; oldest-activity eviction + periodic sweep of expired windows). Over limit → 429 +retry-after. Per-instance; not shared across replicas.Replay protection on payment proofs (single-use, unique DB constraint), 5-minute token expiry, timing-safe HMAC comparison.
GET /v1/statsrequiresx-operator-key: $OPERATOR_KEY(401 otherwise); the comparison is constant-time (SHA-256 digests +crypto.timingSafeEqual).GET /healthis free and unauthenticated: 200{status:'ok',db:'up'}, or 503{status:'degraded',db:'down'}whenSELECT 1fails/times out.Least-privilege DB role.
docker/db/init/01_roles.shcreateslicita_app(CONNECT + SELECT/INSERT/UPDATE/DELETE on all tables, no DDL). SetAPP_DATABASE_URLso app traffic uses it; migrations always run on the adminDATABASE_URL.All inputs zod-validated (max lengths, CPV/date formats); parameterized SQL only; secrets via env only; no keys in any response body.
Error envelope
{ "error": { "code", "message", "hint" } }with an agent-actionable hint.
Deployment
Public-launch production deployment lives in DEPLOYMENT.md: exact one-time and daily command sequences, the migration step, first-ingest, smoke tests, backups, troubleshooting and a mainnet flip checklist.
The production profile (docker-compose.prod.yml) is a single-VM compose
stack: the hardened image + postgres:16-alpine with a named volume. Its
shape:
NODE_ENV=production+PAYMENTS_MODE=x402; every secret is required via compose:?interpolation and enforced at boot byvalidateConfig— nothing has a working default, and the dev faucet route does not exist.Migrations are an explicit
migrateone-shot service (adminDATABASE_URL, exits 0); theappservice starts only after it exitsservice_completed_successfully.App traffic runs as the low-privilege
licita_approle (APP_DATABASE_URL); migrations/DDL always run as the admin user.The app publishes no host port (compose
apphas noports:): a reverse proxy (Caddy/nginx/Cloudflare) reaches it by DNS name over the Docker network and terminates TLS in front. KeepTRUST_PROXY=true(default in the prod file) so rate limiting keys on the real client IP.PLACSP and the daily ingest scheduler are on by default in prod (
PLACSP_ENABLED=true,PLACSP_SCHEDULE=true,INGEST_ON_BOOT=true), each overridable from.env.Postgres binds 127.0.0.1:5432 for host-side ops (
pg_dumpcron,backfill-identityfrom the VM checkout).The image builds with
npm ci, runs as thenodeuser, and carries aHEALTHCHECKhitting/health(busybox wget).
Observability
Every REST request and every MCP tool call writes one async, non-blocking
request_logsrow: source (rest|mcp), endpoint, status, latency, cpv/buyer/company filters, search query text (q),zero_result(empty result set),user_agent, paid flag, error code.Data minimization: paid requests record the pseudonymous payment
client_key(dev token HMAC or x402 payer wallet); unpaid requests recordsha256(ip + OPERATOR_KEY)— raw IPs are never stored in new rows.Every payment (success/failure/replay) writes a
paymentsrow.Structured JSON logs on stdout (payment events, harvest progress, errors).
GET /v1/stats(operator key) returns:unique_clients,requests_by_endpoint(incl. paid counts),requests_by_source,zero_result_queries(count + rate),payment_required_responses,payments(attempts, successes,revenue_usd, by status and by network/provider),repeat_clients(≥2 paid requests + top repeaters),top_searches,unique_user_agents(count + top),top_requested(cpvs/buyers/companies),failed_queries,failed_requests_rate,data_null_rates(award value/winner null share), and the in-memory metrics snapshot.
Testing
npm test # 256 unit tests (vitest, pg-mem + fixtures)
npm run test:api-smoke # server wiring smoke (payment middleware stubbed)
npm run test:integration # REAL app + REAL embedded postgres + live TED slice
npm run smoke # autonomous-agent acceptance test (needs a running,
# ingested instance; BASE_URL env, default :3000)The integration suite starts scripts/dev-db.ts (embedded Postgres 18 on port
5433), runs migrations, ingests 25 live TED notices, boots
buildServer → registerWeb → mountMcp on an ephemeral port, and asserts the
402→faucet→paid flow, envelope shape, replay rejection, renewals and the MCP
tool list. It skips gracefully if api.ted.europa.eu is unreachable.
Limitations (honest list)
PLACSP ingester is opt-in (
PLACSP_ENABLED=falsein the code default; the production profile flips it on withPLACSP_ENABLED=true+PLACSP_SCHEDULE=true). Enable it explicitly for--source placsp|allor scheduled runs. TED remains the default source.No LLM anywhere — search is Postgres FTS (
spanishconfig), forecast signals are deterministic SQL/date math.x402 settlement is tested against a mock facilitator, not the live one — the server-side verify+settle flow (and the client-side smoke agent) are covered by tests, but a run against the public facilitator requires a funded Base Sepolia wallet (see "Run the agent smoke test with real x402 payments").
Framework agreement values are ceiling amounts, not actual spend.
Unit tests use
pg-mem, which lacks window functions and hascount(*) FILTER/ON CONFLICT...RETURNINGquirks — full-app SQL is therefore only covered by the integration suite against real Postgres.scripts/dev-db.ts(embedded-postgres) requires system libraries the bundled binaries link against (e.g. libicu); ifinitdbfails in your environment, usedocker compose up dbinstead.Single-process in-memory rate limiter (per-instance; not shared across replicas).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Related MCP Connectors
MCP server for French (BOAMP) + EU (TED) public procurement data via TenderAPI.
Spain public procurement MCP — PLACSP tender notices (keyless).
TED MCP Server: Real-time EU public tenders access. https://www.lexsocket.ai/
BOE, Spanish public tenders and INE statistics behind one MCP server.
1
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server for querying Spanish government open data APIs including grants, legislation, company registry, statistics, and open data catalog. Enables LLMs to access Spanish public information on-the-fly.265MIT
- AlicenseAqualityDmaintenanceAn MCP server to query Colombian public procurement data from SECOP I and II in real time, enabling contract research, transparency oversight, and data journalism.102MIT
- FlicenseAqualityDmaintenanceMCP server for searching government tenders from CanadaBuys and SAM.gov with free stats and paid search, latest, and AI matching tools using x402 micropayments.41-
- AlicenseAqualityAmaintenanceMCP server providing read access to Swiss public procurement data via the official simap.ch API, covering all cantons and the Confederation with intraday updates. It enables searching tenders, awards, construction codes, and procurement offices through natural language queries.9MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/gastonrey/licita-app'
If you have feedback or need assistance with the MCP directory API, please join our Discord server