Spaces:
Running
Running
File size: 5,143 Bytes
f6f7b53 11dfae8 f6f7b53 11dfae8 f6f7b53 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | ---
title: GreenProof Verification
emoji: π³
colorFrom: green
colorTo: gray
sdk: docker
app_port: 7860
pinned: false
---
# GreenProof verification service
Scores a tree check-in and writes back a confidence, a verdict and a per-signal
breakdown. The only server-side surface in the stack.
## Why this exists
Everything else in GreenProof is Supabase called directly from the browser. This
service exists for one reason: **a client-computed verification result is
forgeable.** The anon key ships inside the PWA and must be assumed public, so
`confidence`, `verdict` and `signals` are not granted to `authenticated` at the
column level. This process holds the only key that can write them.
`POST /score` takes an id and nothing else. Every input is re-read from the
database and from storage β a caller can ask for a check-in to be scored, but
can never influence what the score is.
## Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | `/health` | liveness, model-loaded, config present |
| POST | `/score` | score one check-in β `{"checkin_id": "..."}` |
| POST | `/advise` | species + care advice β **advisory, never a verdict** |
| POST | `/backfill?limit=50` | score everything still `pending` |
| GET | `/docs` | interactive OpenAPI |
`/advise` is the only endpoint that is not verification. It writes
`species_guess` and `advice` and cannot touch `confidence`, `verdict` or
`signals` β enforced by column grants, and by `pipeline.py` never importing the
advisor. It is also the only endpoint that **spends money** (~3 cents a call),
which is why it can be locked behind `ADVISE_TOKEN`.
## How a check-in is judged
**Stage 1 β gates.** Disqualifying on their own, run before any scoring.
| Gate | Catches |
|---|---|
| duplicate image (pHash) | resubmitted photo, gallery photo, internet photo |
| GPS radius | right tree, wrong place |
| travel speed | one account submitting from two impossible places |
**Stage 2 β scores.** Continuous 0β1 signals, weighted into a confidence.
| Score | Signal |
|---|---|
| location | distance from the registered point |
| liveness | excess-green vegetation fraction |
| scene match | DINOv2 cosine + ORB/RANSAC inliers vs previous visits |
| growth | canopy and trunk plausibility, not measurement |
Five of the six fraud types in our attack set are caught in stage 1 by ordinary
deterministic code. That is the honest reason the system works, and it is why
overall accuracy is much better than the 54% rank-1 of image matching alone.
## Honest limits
- Image matching is **corroboration, not identity**. Measured on our own 15
plants: rank-1 54% against 5% chance, AUC 0.77 β real signal, nowhere near
enough to authorise a payout. It is **bimodal**: near-perfect on distinctive
trees, near-zero inside a dense same-species stand.
- Wide-shot matching works partly off the **background**, not the tree.
- Young bark is smooth; BarkNet's ~94% figures are on **mature** bark.
- Thresholds and weights in `scoring.py` are **provisional**. They are replaced
by weights fitted under leave-one-tree-out cross-validation once real rounds
exist. Nothing is tuned on the data used to report performance.
- A missing signal lowers confidence and routes to a human. It never counts as
zero, and it never fails closed on a genuine visit. Designed to degrade.
## Configuration
Space β Settings β Variables and secrets:
| Name | Kind | Value |
|---|---|---|
| `SUPABASE_URL` | variable | your project URL |
| `SUPABASE_SERVICE_KEY` | **secret** | the `service_role` key |
| `GEMINI_API_KEY` | **secret** | for `/advise` β free tier, the default provider |
| `ANTHROPIC_API_KEY` | **secret** | alternative provider, used only if no Gemini key |
| `ADVISOR_PROVIDER` | variable | optional: pin to `gemini` or `anthropic` |
| `ADVISE_TOKEN` | **secret** | any random string; required in `X-Advise-Token` on `/advise` |
| `ALLOWED_ORIGINS` | variable | your Vercel URL |
**The service key must never appear in the frontend.** It can write any verdict
for any tree, and it is the value the whole security model rests on.
**Two providers, one interface.** Advice runs on Gemini's free tier by default,
and falls back to Anthropic if only that key is present. Free tiers have closed
under this project three times mid-build, so a second provider is the cheapest
insurance against a fourth β and switching is one environment variable, not a
code change.
**Setting neither is a supported state, not a failure:** `/score` is unaffected
and `/advise` returns `{"written": false}`. The advisory layer fails soft by
design so it can never take verification down with it.
**`ADVISE_TOKEN` matters on a public Space.** Without it, anyone who finds this
URL and a valid check-in id can spend your Anthropic credit three cents at a
time. `/score` needs no such lock: it costs only our own CPU.
## Local run
```
cd ml
pip install -r requirements.txt
export SUPABASE_URL=... SUPABASE_SERVICE_KEY=...
uvicorn app:app --reload --port 7860
```
|