Early-phase cost prediction
Turn a plain-language building description into an early-phase cost estimate.
The idea: embed the descriptions of projects you have already built or
calculated, store them with their known EUR/m² values, and estimate a new
project as the similarity-weighted average of its nearest neighbors. This is
nearest-neighbor lookup over /v1/embeddings
vectors, not model training, and the estimate is only as good as the reference
data behind it. With your own project history in the table, it gives a
defensible first number in seconds.
Prerequisites and cost
Section titled “Prerequisites and cost”- Create an account at console.flinq.ai/signup and create an API key. You get EUR 5 starting credit, no card required.
- Python 3.10+ with
requestsandpsycopg(pip install requests "psycopg[binary]"). - A local PostgreSQL with pgvector:
docker run -d --name recipe-pg -e POSTGRES_PASSWORD=flinq \ -p 5432:5432 pgvector/pgvector:pg17Cost of the full run: embedding 24 reference descriptions plus one query is about 1,000 tokens at EUR 0.40 per 1M tokens, well under one cent.
Sample data
Section titled “Sample data”sample-kennwerte.csv holds 24 synthetic
reference projects (project,description,bgf_m2,eur_per_m2,year) across
industrial, office, education and residential typologies. The EUR/m² values
are invented demonstration figures, not market data. Replace them with your
own calculated projects to get estimates you can stand behind.
Step 1: Create the table
Section titled “Step 1: Create the table”CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE reference_projects ( id serial PRIMARY KEY, project text, description text, bgf_m2 numeric, eur_per_m2 numeric, year int, embedding vector(1024));Run it with docker exec -i recipe-pg psql -U postgres or any SQL client. The
column is vector(1024) because flinq-pilot-otter returns 1024 dimensions
by default; if you request a smaller Matryoshka slice, size
the column to match.
Step 2: Embed the references
Section titled “Step 2: Embed the references”One batch request embeds all 24 descriptions; the per-request cap for
flinq-pilot-otter is 2048 inputs.
curl https://api.flinq.ai/v1/embeddings \ -H "Authorization: Bearer flq_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "model": "flinq-pilot-otter", "input": [ "Eingeschossige Produktionshalle in Stahlbauweise, Sandwichfassade, einfacher Ausbau", "Viergeschossiger Büroneubau, Stahlbetonskelett mit Lochfassade, mittlerer Ausbaustandard" ] }'import requests
def embed(texts): r = requests.post( "https://api.flinq.ai/v1/embeddings", headers={"Authorization": "Bearer flq_your_key_here"}, json={"model": "flinq-pilot-otter", "input": texts}, ) r.raise_for_status() data = sorted(r.json()["data"], key=lambda d: d["index"]) return [d["embedding"] for d in data]async function embed(texts) { const r = await fetch('https://api.flinq.ai/v1/embeddings', { method: 'POST', headers: { Authorization: 'Bearer flq_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'flinq-pilot-otter', input: texts }), }); const { data } = await r.json(); return data.sort((a, b) => a.index - b.index).map((d) => d.embedding);}Then read the CSV and insert each row with its vector. pgvector accepts the
vector as its text form, so str(vec) on the Python list is enough:
import csv, psycopg
with psycopg.connect(DSN) as conn: with open("sample-kennwerte.csv", newline="", encoding="utf-8") as f: rows = list(csv.DictReader(f)) vectors = embed([row["description"] for row in rows]) with conn.cursor() as cur: for row, vec in zip(rows, vectors): cur.execute( "INSERT INTO reference_projects" " (project, description, bgf_m2, eur_per_m2, year, embedding)" " VALUES (%s, %s, %s, %s, %s, %s)", (row["project"], row["description"], row["bgf_m2"], row["eur_per_m2"], row["year"], str(vec)), ) conn.commit()Step 3: Estimate a new project
Section titled “Step 3: Estimate a new project”Embed the new description with the same embed call as in step 2, then let
pgvector find the nearest references. The <=> operator is cosine distance,
so 1 - distance is cosine similarity:
SELECT project, eur_per_m2, 1 - (embedding <=> %s) AS similarityFROM reference_projectsORDER BY embedding <=> %sLIMIT 5;The estimate is the similarity-weighted average over those neighbors:
def estimate(conn, description, k=5): vec = str(embed([description])[0]) with conn.cursor() as cur: cur.execute( "SELECT project, eur_per_m2, 1 - (embedding <=> %s) AS similarity" " FROM reference_projects ORDER BY embedding <=> %s LIMIT %s", (vec, vec, k), ) neighbors = cur.fetchall() total_weight = sum(sim for _, _, sim in neighbors) weighted = sum(float(eur) * sim for _, eur, sim in neighbors) / total_weight return weighted, neighborsReading the result
Section titled “Reading the result”For the query “Neubau einer beheizten Produktionshalle mit zweigeschossigem Büro- und Sozialanbau, Stahlbeton-Fertigteile, mittlerer Standard”, the script below prints this actual output against the sample data:
24 reference projects embedded and stored
reference project similarity EUR/m2Produktionshalle mit Buerotrakt 0.76 1240Werkhalle Nord Gewerbepark 0.58 980KFZ-Werkstatt mit Annahme 0.55 1420Bauhof Kommunal 0.55 1350Kalthalle Baustoffhandel 0.52 540
weighted estimate: 1120 EUR/m2 BGFThe nearest neighbor is the one reference with the same typology, a production hall with an attached office tract, and the estimate lands between the plain halls and the office-heavy references. On your own data, measure MAE (mean absolute error in EUR/m²) and Spearman rank correlation against held-out projects before you trust the numbers; see Use cases.
The full script
Section titled “The full script”The script embeds the references once: on later runs the table is already filled and only the query embedding costs tokens.
"""Early-phase cost estimate from reference projects via flinq embeddings + pgvector."""import csvimport os
import psycopgimport requests
API = "https://api.flinq.ai"KEY = os.environ["FLINQ_API_KEY"] # create a key at https://console.flinq.aiHEADERS = {"Authorization": f"Bearer {KEY}"}MODEL = "flinq-pilot-otter"DSN = os.environ.get("PG_DSN", "postgresql://postgres:flinq@localhost:5432/postgres")
def embed(texts): r = requests.post( f"{API}/v1/embeddings", headers=HEADERS, json={"model": MODEL, "input": texts}, ) r.raise_for_status() data = sorted(r.json()["data"], key=lambda d: d["index"]) return [d["embedding"] for d in data]
def load_references(conn): with open("sample-kennwerte.csv", newline="", encoding="utf-8") as f: rows = list(csv.DictReader(f)) vectors = embed([row["description"] for row in rows]) with conn.cursor() as cur: for row, vec in zip(rows, vectors): cur.execute( "INSERT INTO reference_projects" " (project, description, bgf_m2, eur_per_m2, year, embedding)" " VALUES (%s, %s, %s, %s, %s, %s)", (row["project"], row["description"], row["bgf_m2"], row["eur_per_m2"], row["year"], str(vec)), ) conn.commit() return len(rows)
def estimate(conn, description, k=5): vec = str(embed([description])[0]) with conn.cursor() as cur: cur.execute( "SELECT project, eur_per_m2, 1 - (embedding <=> %s) AS similarity" " FROM reference_projects ORDER BY embedding <=> %s LIMIT %s", (vec, vec, k), ) neighbors = cur.fetchall() total_weight = sum(sim for _, _, sim in neighbors) weighted = sum(float(eur) * sim for _, eur, sim in neighbors) / total_weight return weighted, neighbors
def main(): new_project = ( "Neubau einer beheizten Produktionshalle mit zweigeschossigem " "Büro- und Sozialanbau, Stahlbeton-Fertigteile, mittlerer Standard" ) with psycopg.connect(DSN) as conn: with conn.cursor() as cur: cur.execute("SELECT count(*) FROM reference_projects") stored = cur.fetchone()[0] if stored: print(f"{stored} reference projects already stored\n") else: n = load_references(conn) print(f"{n} reference projects embedded and stored\n") est, neighbors = estimate(conn, new_project) print(f"{'reference project':<38} {'similarity':>10} {'EUR/m2':>8}") for project, eur, sim in neighbors: print(f"{project:<38} {sim:>10.2f} {float(eur):>8.0f}") print(f"\nweighted estimate: {est:.0f} EUR/m2 BGF")
if __name__ == "__main__": main()export FLINQ_API_KEY=flq_your_key_herepython predict_cost.pyAdapt it
Section titled “Adapt it”- Replace the CSV with your own calculated projects. A few dozen references already work; the estimate sharpens as the table grows.
- At this scale a sequential scan is instant. Past roughly 100k rows, add an
HNSW index:
CREATE INDEX ON reference_projects USING hnsw (embedding vector_cosine_ops); - To shrink storage, request
"dimensions": 256in the embed call and declare the columnvector(256); see Models for the trade-off. - The same pattern works one level down: embed calculated LV positions with their unit prices as references, and price new positions by nearest neighbors. Pair it with LV matching to price a whole incoming tender.