Veritris Energy — AXIOM Exchange API Platform
An energy marketplace/exchange API, domain-modeled after the plan/utility/ usage/enrollment structure exposed by PowerHQ's public developer docs (powerhq.co, built on EnergyBot's backend), extended with:
- Reseller multi-tenancy: one reseller account serving many customers across many states, each customer holding many properties. A customer attaches to many resellers, and each attachment carries its own cost key identifying the reseller for cost attribution.
- A generic geospatial asset registry — any asset type, any geometry shape (point/line/polygon/multipolygon) — with GeoJSON and Esri JSON output so any property can be dropped straight onto an ArcGIS map.
- Spatial customer search and utility status by area, so a reseller can select a region on a map and see who they have there and what the utility situation is.
- An AXIOM Exchange layer for counterparties and nominations/trades/ settlements, tied back to physical assets and cost keys.
This isn't just a design doc. Every endpoint has been run end-to-end against a live PostgreSQL/PostGIS instance, with five real bugs found and fixed along the way. See the Verification Log for the full details.
↗ Try the Territory Explorer — an interactive map demo driven by a snapshot of real seeded output: scope to a reseller, drag a box over any area, and see the customers and utility status inside it.
Documentation
| Doc | Covers |
|---|---|
| API Reference | Every endpoint, with real captured request/response examples |
| ArcGIS Integration | Wiring AXIOM into the ArcGIS JS API, Esri JSON conversion, publishing to ArcGIS Online |
| Data Model | The tenancy model, why one generic assets table, why Postgres+PostGIS+JSONB |
| Deployment | Local, on-prem, AWS, Azure, and Kong deployment, plus the environment variable reference |
| Verification Log | What was actually verified, and the five bugs found + fixed while doing it |
The tenancy model in one paragraph
A reseller is the account holder and the tenant boundary. A customer
is deliberately not owned by a single reseller — it attaches to many, one
row per relationship in customer_resellers, and the cost key lives on
that relationship rather than on either side. The key defaults to the
reseller's UUID so it always resolves back to a reseller row, and can be
overridden with a reseller's own billing code. A property is an asset
carrying reseller_id, customer_id, state, and the derived cost_key.
One customer account can therefore hold many properties spread across many
states, each attributable to whichever reseller services it. Reseller-scoped
callers see only their own data — including through spatial queries, and
including seeing only their own side of a customer shared with a competitor.
Why this stack
| Requirement | Choice | Why |
|---|---|---|
| Structured + unstructured data | PostgreSQL 16 + PostGIS + JSONB | One database instead of two. Relational tables sit next to a JSONB attributes column on every table for whatever unstructured fields a record needs — no schema migration per new field. Same flexibility Cosmos DB/DynamoDB give you, without giving up joins, transactions, and geospatial indexing. |
| Geospatial polygons for ArcGIS | PostGIS + GeoAlchemy2 + Shapely | Native ST_Intersects/ST_DWithin/ST_Contains; output as RFC 7946 GeoJSON (consumed directly by the ArcGIS JS API's GeoJSONLayer) or Esri JSON for ArcGIS REST/Runtime clients. |
| Portable to Aurora / managed cloud DB | Aurora PostgreSQL or Azure Database for PostgreSQL | Both wire-compatible with vanilla PostgreSQL and both support PostGIS as a managed extension — AXIOM_DATABASE_URL is the only thing that changes. |
| Load balancing | nginx | In front of N stateless API replicas locally, or inside the cluster/VPC in the cloud. |
| Pluggable into an API Gateway | Stateless JWT/API-key auth + OpenAPI 3 spec | No session state, so any number of instances can sit behind AWS API Gateway, Azure APIM, or Kong. Reseller scope travels in the JWT claim or the X-Axiom-Reseller-Id header. |
Architecture
ArcGIS Online / ArcGIS JS API (GeoJSONLayer) ─┐
Web / mobile clients ─────────────────────────┼──▶ API Gateway (optional)
Service-to-service callers ───────────────────┘ (TLS, auth, quota, WAF)
│
▼
nginx (load balancer)
│ │
▼ ▼
api1 (FastAPI) api2 (FastAPI)
│ │
└──────┬───────┘
▼
PostgreSQL + PostGIS (+ JSONB)
Redis (cache / rate-limit state)
Run it locally
cp .env.example .env # edit AXIOM_JWT_SECRET at minimum
docker compose up --build
API (via nginx LB):
http://localhost:8080/api/v1/...Swagger UI:
http://localhost:8080/docsSeed the demo dataset (3 resellers, 6 utilities with service status, 6 customers, 14 properties across TX/OK/NM/KS):
docker compose exec api1 python -m app.db.seed_demo
Try the reseller + spatial endpoints
K='X-API-Key: axiom-local-dev-key'
# Every cost key, the reseller it bills to, and what it covers:
curl http://localhost:8080/api/v1/cost-keys -H "$K"
# One customer's full reseller attachment list (the one-to-many view):
curl http://localhost:8080/api/v1/customers/<customer_id> -H "$K"
# Customers holding properties inside an area:
curl -X POST http://localhost:8080/api/v1/geo/customers/search -H "$K" \
-H "Content-Type: application/json" \
-d '{"geometry":{"type":"Polygon","coordinates":[[[-98.2,29.9],[-96.4,29.9],[-96.4,33.3],[-98.2,33.3],[-98.2,29.9]]]}}'
# Utility service status across that same area:
curl -X POST http://localhost:8080/api/v1/geo/utilities/status -H "$K" \
-H "Content-Type: application/json" \
-d '{"geometry":{"type":"Polygon","coordinates":[[[-98.2,29.9],[-96.4,29.9],[-96.4,33.3],[-98.2,33.3],[-98.2,29.9]]]}}'
# A reseller-scoped token — every call above then filters to that reseller:
curl -X POST http://localhost:8080/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"api_key":"axiom-local-dev-key","reseller_id":"<reseller_uuid>"}'
Every geo endpoint accepts ?format=geojson (default) or ?format=esri.
Scaling out
- API layer: stateless — add replicas.
WEB_CONCURRENCYcontrols gunicorn workers per container. - Cloud: run the API container behind an autoscaling group (ECS Fargate, Azure Container Apps, k8s + HPA) fronted by the cloud load balancer. nginx can stay as an internal layer or be dropped.
- Database: point
AXIOM_DATABASE_URLat Aurora PostgreSQL or Azure Database for PostgreSQL with PostGIS enabled. Read replicas can take the read-heavy/geo/*traffic.
Notes on the domain model
- Assets are deliberately generic. One
assetstable, discriminated byasset_type, with a single PostGISGEOMETRYcolumn that holds a point, line, or polygon depending on what the asset is, plusJSONB attributesfor type-specific fields. New asset types never require a migration. - A "premise" is just an asset (
asset_type='premise'), so usage records and enrollments work for any point asset without special-casing. - Cost keys are derived, not trusted. The API resolves a property's cost key from the customer's attachment for that reseller and state, so a property can never be stamped with a key that doesn't correspond to a real relationship.
- This is a working scaffold, not a finished production system: add Alembic migrations, per-route authorization scopes, and an automated test suite before handling real trading volume or customer data.