Inspiration

What it does

How we built it

Challenges we ran into

Accomplishments that we're proud of

What we learned

What's next for Costa# Costa

Real weather in. Your kit on file. A resource plan out.


Inspiration

Small islands run the hardest energy and water problem there is, and they run it with the least margin. A city that misjudges demand borrows power from the grid next door. An island cannot borrow anything. Diesel arrives by barge, fresh water often has to be manufactured from seawater, and the machine that manufactures it is one of the largest loads on the grid. Energy and water are not two problems on an island they are one problem wearing two hats.

So the operator's real question is never "how much sun will there be tomorrow." It's "given the weather that is actually coming, and the specific hardware I actually own, what breaks first, and what should I buy to fix it?"

That question has a shape we recognized: it needs live environmental data, a physical model of the operator's own equipment, and something that can explain the result to a person who has to justify it to a town council. We could not find a tool that did all three and showed its work. So we built Costa.

We also started from a conviction about the AI. There are plenty of tools that ask a language model to guess at how much sun an island will get. We think that is exactly backwards. A model that hallucinates a solar figure is worse than no tool at all, because it is confidently wrong to a person making a real decision about real people. We wanted the arithmetic to be auditable and the AI to be strictly a translator.


What it does

You lock a real point on Earth, describe the hardware you actually own, and Costa returns an operating picture:

  • Live environmental state — irradiance, wind, precipitation, temperature, and where available wave height and sea surface temperature.
  • A modeled 24-hour generation curve for your solar and wind capacity, not a generic one.
  • Days of water security from your storage, your desalination rate, and forecast rainfall.
  • A resilience score with a five-part breakdown, so a bad number always tells you which part is bad.
  • Ranked upgrades — the specific purchase that moves the score most, with the predicted delta.
  • A timed action plan ("run desalination in this window, hold reserve after this hour") tied to the forecast hour that justifies it.

Every number traces to a source. The AI writes the briefing, and nothing else.


How we built it

Stack. React 19 + Vite 7 + Tailwind on the front, Hono on @hono/node-server on the back, SQLite through @libsql/client, Recharts for the curves, Leaflet for the map. Auth is bcrypt with a session cookie. It runs from npm install && npm run dev with no cloud dependency and no database to provision.

Data sources, all keyless. Open-Meteo for forecast, Open-Meteo Marine for waves and sea surface temperature, NASA POWER for a 30-day climatological baseline, Nominatim for geocoding. Anyone can clone this and run it for free, anywhere on Earth.

The architecture is deliberately narrow. One endpoint does the real work:

Open-Meteo ─┐
Marine ─────┼─→ Promise.allSettled ─→ buildAnalysis() ─→ one JSON document
NASA POWER ─┘                        (pure, deterministic)      │
                                                                ▼
                                                    Grok (prose only) ─→ React

buildAnalysis() is a pure function. Same inputs, same outputs, no network, no AI, no clock beyond the timestamp. That decision paid for itself repeatedly, it made the model testable, made the scenario engine trivial, and made the recommender possible.

The physics

Solar output derates plane-of-array irradiance by a performance ratio covering inverter loss, thermal derate, and soiling:

$$P_{\text{solar}} = C_{\text{solar}} \cdot \frac{G}{1000} \cdot \eta_{PR}, \qquad \eta_{PR} = 0.8$$

Wind is the piecewise curve a real turbine follows. Below cut-in there is not enough torque; above rated the turbine spills; above cut-out it brakes to protect itself. Between cut-in and rated, power goes as the cube of wind speed, which is the single most important fact about wind and the reason a modest gust forecast matters so much:

$$ P_{\text{wind}} = \begin{cases} 0 & v < 3 \text{ m/s} \ \text{ or } \ v \geq 25 \text{ m/s} \[4pt] C_{\text{wind}} \left( \dfrac{v - 3}{12 - 3} \right)^{3} & 3 \leq v < 12 \text{ m/s} \[8pt] C_{\text{wind}} & 12 \leq v < 25 \text{ m/s} \end{cases} $$

Load scales with people and with what the island refuses to switch off:

$$L = 4\,\text{kW} + 0.12 \cdot (N_{\text{perm}} + N_{\text{temp}}) + 1.1 \cdot S_{\text{enabled}}$$

Water is a balance, not a level. Demand at $50$ L/person/day runs against desalination plus whatever the forecast rain will actually deliver into an $80\,\text{m}^2$ catchment at $80\%$ efficiency:

$$D = 50(N_{\text{perm}} + N_{\text{temp}}), \qquad R = 0.8 \cdot 80 \cdot \sum_{h=1}^{24} p_h$$

$$T_{\text{water}} = \frac{V_{\text{stored}}}{D - (Q_{\text{desal}} + R)}$$

When the denominator goes non-positive the island is net-positive on water and we cap the horizon rather than report infinity.

The score

Five sub-scores, each clamped to $[0, 100]$, each independently legible:

$$ \begin{aligned} E &= 70 \cdot \frac{\sum_{h=1}^{24} P_h}{24L} + \frac{4 \cdot C_{\text{batt}}}{\max(L, 1)} &\quad&\text{(energy)}\[6pt] W &= 100 \cdot \frac{T_{\text{water}}}{7} &\quad&\text{(water, 7 days = full marks)}\[6pt] C &= 100 - \min\left(60,\ 80 \cdot \frac{\left| \bar{G}{24} - \mu{\text{NASA}} \right|}{\mu_{\text{NASA}}}\right) &\quad&\text{(climate anomaly)}\[6pt] I &= 100 \cdot \frac{S_{\text{enabled}}}{S_{\text{total}}} &\quad&\text{(critical services)}\[6pt] R &= \frac{C_{\text{solar}} + C_{\text{wind}}}{2} + 0.4 \cdot \rho_{\text{reserve}} &\quad&\text{(renewable posture)} \end{aligned} $$

$$\boxed{\ \text{Resilience} = 0.28E + 0.24W + 0.16C + 0.16I + 0.16R\ }$$

The climate term is the one we are proudest of. It asks whether tomorrow departs from the 30-day satellite normal for that exact coordinate. That is what turns a forecast into a judgment: 4 kWh/m² is a fine day in Reykjavík and an alarming one in Malta, and only the NASA baseline knows the difference.

The recommender

This is the part that surprised us with how simple it turned out to be. Because buildAnalysis() is pure, "what should I buy?" is not a heuristic or a lookup table or a prompt. It is the scoring function run again on counterfactual hardware:

$$\Delta_i = f(\mathbf{x} + \delta_i) - f(\mathbf{x})$$

We evaluate five candidate upgrades, $+25$ kW solar, $+50$ kWh battery, $+50$ kW wind, $+20{,}000$ L storage, $+5{,}000$ L/day desalination re-running the full model against the same real weather for each, then sort by $\Delta$. The advice is therefore never a guess. It is the same arithmetic, asked a different question.

Keeping the AI in its lane

Grok receives the finished JSON and a hard instruction that it may not invent a single environmental value. Every AI response ships with a transparency object naming the provenance of each layer: environmental data from Open-Meteo and NASA, infrastructure from the user's own configuration, numbers from the deterministic engine, language from Grok. Pull the API key out and Costa still computes everything — you simply lose the prose. We consider that the correct failure mode, and we built it that way on purpose.


Challenges we ran into

Calibrating a score so it actually says something. This was the hard one, and it was not a coding problem. Our first defaults produced critical at every location on Earth — and for a while we thought the model was broken. It wasn't. It was correct and useless. Working the arithmetic backwards: 1,200 people need $60{,}000$ L/day, our default desalination supplied $8{,}000$, so water security was always $\approx 0.96$ days. The same population implies a $153$ kW load, or $3{,}684$ kWh/day, against roughly $240$ kWh from 80 kW of solar an 8× shortfall. Four different islands, four different weathers, identical verdict.

Then we over-corrected and hit the opposite failure: with generous hardware every sub-score pinned at 100, and every upgrade delta collapsed to zero. The recommender went silent, because when you are already perfect nothing improves you.

The lesson generalizes past this project: a metric that cannot go down is not measuring anything, and neither is one that cannot go up. We now deliberately target the band where the score has room to move in both directions, and we treat the default configuration as a design artifact that deserves as much care as the model itself.

Missing data that does not announce itself. NASA POWER encodes absent readings as -999, not null. Ingested naively, a single missing irradiance hour drags a daily mean deep into nonsense and quietly poisons the climate anomaly downstream. We wrote one sentinel filter reject anything $\leq -900$ or $\geq 9000$, return null and routed every upstream number through it. Nulls then propagate honestly all the way to the UI, which renders "unavailable" instead of a confident lie.

Three upstreams, three independent ways to fail. Marine coverage simply does not exist for inland or sheltered coordinates. NASA POWER rate-limits. Any of them can time out. Promise.allSettled with per-source status meant a dead feed degrades one panel instead of blanking the dashboard, and the source strip tells you exactly which one is down. Costa still produces a full energy and water plan for a landlocked point — it just says so about the waves.

A bug that only existed between two pages. Our landing page links to preset locations as /signup?site=San Juan. Signup authenticates and redirects to the dashboard, which bounces an islandless user to onboarding and that second redirect silently drops the query string. Onboarding read null, prefilled nothing, and Number("") evaluated to 0, so a preset click created an island at $0°, 0°$: open ocean in the Gulf of Guinea. Every individual page was correct. The bug lived in the seam between them, and only a full click-through found it, which is why we now walk the entire path from cold start before every demo.

Nominatim rejects anonymous traffic. OpenStreetMap's geocoder requires a real User-Agent identifying your application. A small thing, but it cost us an afternoon of opaque failures, and it taught us to read the etiquette page of a free API before its endpoint reference.


What we learned

Purity is a feature, not an aesthetic. Making buildAnalysis() free of network, clock, and AI was a five-minute decision that paid out three times: scenarios became "call it again with different inputs," the recommender became "call it again five times," and correctness became something we could actually reason about. Almost every good property Costa has traces back to that one constraint.

The interesting question is the counterfactual. A dashboard tells you the state of the world. What an operator needs is the derivative — if I change this, what happens? Once we had a deterministic model, that stopped being an AI problem and became a for-loop.

AI is at its best when it is not the smart part. Costa's language model cannot compute, cannot fetch, and cannot invent. It reads a finished document and explains it. That constraint made the product more trustworthy, not less capable, and it means a wrong answer is traceable to a line of arithmetic we can point at and fix.

Physical models expose their own assumptions. The cubic wind curve makes it immediately obvious why a 5 m/s forecast is nearly worthless and 11 m/s is transformative. Writing the equations down forced us to be explicit about every constant, which is why Costa ships its assumptions list in the UI rather than burying it.


What's next

  • Fix the score's dynamic range properly — normalize sub-scores against island size so a 400-person atoll and a 40,000-person island are graded on comparable curves.
  • Tropical cyclone ingestion for genuine multi-day evacuation and pre-positioning windows.
  • Battery dispatch optimization — right now we report a reserve floor; we want to solve for the actual charge/discharge schedule against the forecast.
  • Measured catchment area instead of the documented $80\,\text{m}^2$ default, which is currently our least defensible constant.
  • Multi-island fleets, so a national utility can triage which island needs the barge first.

Built with

react · typescript · vite · tailwindcss · hono · sqlite · recharts · leaflet · framer-motion · open-meteo · nasa-power · nominatim · grok · xai

Share this project:

Updates

Submission history