Skip to content

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.

  1. Create an account at console.flinq.ai/signup and create an API key. You get EUR 5 starting credit, no card required.
  2. Python 3.10+ with requests and psycopg (pip install requests "psycopg[binary]").
  3. A local PostgreSQL with pgvector:
docker run -d --name recipe-pg -e POSTGRES_PASSWORD=flinq \
-p 5432:5432 pgvector/pgvector:pg17

Cost 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-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.

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.

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"
]
}'

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()

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 similarity
FROM reference_projects
ORDER BY embedding <=> %s
LIMIT 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, neighbors

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/m2
Produktionshalle mit Buerotrakt 0.76 1240
Werkhalle Nord Gewerbepark 0.58 980
KFZ-Werkstatt mit Annahme 0.55 1420
Bauhof Kommunal 0.55 1350
Kalthalle Baustoffhandel 0.52 540
weighted estimate: 1120 EUR/m2 BGF

The 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 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 csv
import os
import psycopg
import requests
API = "https://api.flinq.ai"
KEY = os.environ["FLINQ_API_KEY"] # create a key at https://console.flinq.ai
HEADERS = {"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_here
python predict_cost.py
  • 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": 256 in the embed call and declare the column vector(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.