Veritris Axiom — Exchange API
Axiom Exchange is the connected layer of the Veritris Axiom energy platform — energy data, transactions, partners, developers, and API-first infrastructure. It sits under Veritris Energy alongside OEPC (the utility-facing operating vehicle), and is one module among Grid, Market/ETRM, Forecast, DER, Retail/CRM, Resilience, eMobility, and Orchestrate.
The API is 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 drops 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.
- A saved-query registry in its own separate database, addressable by
query_id, stateful (frozen result) or stateless (replays live). - An exchange layer for counterparties and nominations/trades/settlements, tied back to physical assets and cost keys.
- A database, load-balancing, and deployment shape built to run unmodified on a laptop or behind a cloud API Gateway.
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
docs/TESTING.mdfor the full verification log.
↗ 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.
↗ Try the Developer Portal — API catalog, a real
username/password login (Twilio-Verify-ready 2FA, inert until keys are
configured), a dashboard, and an API Explorer, all wired to the real
scope/metering backend. Defaults to a live deployment at
axiom-exchange-api.onrender.com
— sign in and every number is real, read from GET /usage/me; clear the
API base URL field to fall back to a demo mode instead. That default is a
free-tier Render service, so it spins down after 15 minutes idle (~30–60s
cold start on the next request) and shares its Postgres server with the
saved-query/metering database rather than running on the fully separate
instance the architecture describes — both noted honestly in
docs/TESTING.md and docs/DATA_MODEL.md rather than glossed over.
Signing in to the portal also unlocks Swagger, /openapi.json, and the
Deployment / Auth Architecture docs — those moved off this public site
entirely and are now served only through an authenticated backend
endpoint. This is the P0 "developer foundation" slice, not the full
commercial console — see docs/TESTING.md for exactly what is and isn't
real.
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.
Authentication & commercial access in one paragraph
Every operation requires a specific OAuth-style scope — visible
per-operation in /openapi.json's security blocks — on top of a valid
credential; being authenticated is necessary but never sufficient.
Privileged capabilities (trading:execute, der:control,
settlement:write, grid:control, admin) additionally require a
production-environment credential, enforced in code, not left to
policy. Every authorized, billable request is metered into a separate
usage-events store, billed by scope rather than by raw endpoint path.
MIT-licensed SDKs exist for Python, JavaScript, and PHP (sdks/) — they
are thin transport wrappers; every call they make still hits the
authenticated, metered, proprietary API. Full model, what's implemented
vs. target OIDC federation, and the documentation-tier split:
Auth Architecture and API Tiers — the former now requires signing in
through the Developer Portal (standard username/password,
Twilio-Verify-ready 2FA), the latter stays public at
docs/API_TIERS.md.
Positioning constraints
Veritris, Veritris Energy, and OEPC are not represented as a regulated utility, retail electric provider, RTO/ISO, telecom carrier, or authorized market participant anywhere in this repository — those statuses require independent verification. Axiom Exchange interoperates with market and utility systems; it is not a market operator. Retail plan and enrollment endpoints existing in the API is not a representation that competitive retail choice is available in any particular jurisdiction. No ® is used on "Veritris" or "Axiom" here pending trademark clearance.
Contact
Veritris Group, Inc. — veritris.com
| energy@veritris.com | |
| Phone | +1 580-713-4927 |
| Address | 2828 NW 57th Street #207, Oklahoma City, OK 73112 |
| Contact page | veritris.com/lawton-managed-msp-contact |
| Client / support portal | veritris.com/clients |
| Veritris Energy | veritris.energy |
| Axiom platform | veritrisaxiom.energy |
Also available programmatically at GET /api/v1/contact (unauthenticated).
Documentation
| Doc | Covers |
|---|---|
docs/API_REFERENCE.md |
Every endpoint, with real captured request/response examples |
| Auth Architecture (portal login required) | Full OAuth scope/role model, what's implemented vs. target OIDC federation, usage metering — sign in at the portal (defaults to the live deployment) to read it |
docs/API_TIERS.md |
Public/customer/partner/trading documentation classification |
docs/PRICING.md |
Subscription tiers and per-call metering reference |
docs/LICENSING.md |
License-type concepts for counsel to formalize (not a binding agreement) |
docs/ARCGIS_INTEGRATION.md |
Wiring AXIOM into the ArcGIS JS API, Esri JSON conversion, publishing to ArcGIS Online |
docs/DATA_MODEL.md |
Why one generic assets table, why Postgres+PostGIS+JSONB instead of a second NoSQL store |
| Deployment (portal login required) | Local, on-prem, AWS, Azure, and Kong deployment, plus the full environment variable reference — sign in at the portal to read it |
docs/TESTING.md |
What was actually verified, and the bugs found + fixed while doing it |
Why this stack
| Requirement | Choice | Why |
|---|---|---|
| Structured + unstructured data | PostgreSQL 16 + PostGIS + JSONB | One database instead of two. Relational tables (plans, rates, enrollments, transactions) sit next to a JSONB attributes column on every table for whatever unstructured fields a given record needs (well API numbers, permit conditions, meter specs...) — no schema migration per new field. This is the same pattern Cosmos DB/DynamoDB give you for unstructured data, without giving up joins, transactions, and geospatial indexing for the structured side. |
| Geospatial polygons for ArcGIS | PostGIS geometry columns + GeoAlchemy2 + Shapely | Native ST_Intersects/ST_DWithin/ST_Contains spatial queries; output as RFC 7946 GeoJSON (consumed directly by the ArcGIS JS API's GeoJSONLayer) or converted to Esri JSON for older ArcGIS REST/Runtime clients. |
| Portable to Aurora / Cosmos-class managed DB | Amazon Aurora PostgreSQL-Compatible or Azure Database for PostgreSQL | Both are wire-compatible with vanilla PostgreSQL and both support PostGIS as a managed extension — AXIOM_DATABASE_URL is the only thing that changes between your laptop and either cloud. |
| Load balancing | nginx | Sits in front of N API replicas locally (api1, api2) or inside the cluster/VPC in the cloud, in front of an autoscaled service. |
| Pluggable into an API Gateway | Stateless JWT/API-key auth + auto-generated OpenAPI 3 spec | The service never holds session state, so any number of instances can sit behind AWS API Gateway, Azure APIM, or Kong. Import /openapi.json directly into AWS API Gateway or Azure APIM, or see gateway/kong.example.yaml for a Kong config. |
Architecture
ArcGIS Online / ArcGIS JS API (GeoJSONLayer) ─┐
Web / mobile clients ─────────────────────────┼──▶ API Gateway (optional)
Service-to-service callers ───────────────────┘ (AWS API GW / Azure APIM / Kong:
TLS, auth, quota, WAF)
│
▼
nginx (load balancer)
least_conn, health-aware
│ │
▼ ▼
api1 (FastAPI) api2 (FastAPI) ← scale out N ways
│ │
└──────┬───────┘
▼
PostgreSQL + PostGIS (+ JSONB)
── Aurora PostgreSQL / Azure DB for
PostgreSQL in the cloud
Redis (cache / rate-limit state)
Repository layout
axiom-exchange/
├── api/
│ ├── app/
│ │ ├── core/ settings, DB session, auth
│ │ ├── models/ SQLAlchemy + GeoAlchemy2 models (assets, utilities, plans, usage, exchange)
│ │ ├── schemas/ Pydantic request/response + GeoJSON schemas
│ │ ├── services/ geo_service.py — GeoJSON <-> PostGIS <-> Esri JSON
│ │ ├── api/v1/routers/ assets, geo, tenancy, queries, utilities, plans,
│ │ │ usage, enrollments, exchange, auth, contact, health
│ │ ├── db/ schema.sql (canonical DDL) + seed_demo.py
│ │ └── main.py
│ ├── Dockerfile
│ └── requirements.txt
├── nginx/nginx.conf load balancer config
├── gateway/kong.example.yaml example API Gateway config
├── docker-compose.yml postgres + redis + 2 api replicas + nginx
└── .env.example
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/docs— now gated behind a portal login (standard username/password, not third-party OIDC). A dev seed user (axiom-admin/change-me-axiom-dev) is created automatically onAXIOM_ENVIRONMENT=localstartup; change or remove it before this ever runs against real data.Seed 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 ArcGIS-facing endpoints
# List assets, GeoJSON:
curl http://localhost:8080/api/v1/assets?asset_type=solar_farm -H "X-API-Key: axiom-local-dev-key"
# A single asset as a GeoJSON Feature (point this URL at an ArcGIS JS API GeoJSONLayer):
curl http://localhost:8080/api/v1/assets/<asset_id>/geometry -H "X-API-Key: axiom-local-dev-key"
# Same asset as Esri JSON:
curl "http://localhost:8080/api/v1/assets/<asset_id>/geometry?format=esri" -H "X-API-Key: axiom-local-dev-key"
# Draw/paste a polygon and get every intersecting asset back as a FeatureCollection:
curl -X POST http://localhost:8080/api/v1/geo/query \
-H "X-API-Key: axiom-local-dev-key" -H "Content-Type: application/json" \
-d '{"geometry": {"type": "Polygon", "coordinates": [[[-97.9,30.15],[-97.6,30.15],[-97.6,30.45],[-97.9,30.45],[-97.9,30.15]]]}}'
# Load whatever's in the current map viewport:
curl "http://localhost:8080/api/v1/geo/bbox?min_lon=-98&min_lat=30&max_lon=-97.4&max_lat=30.6" \
-H "X-API-Key: axiom-local-dev-key"
# Utility lookup by point (mirrors PowerHQ's "find the utility for this address"):
curl "http://localhost:8080/api/v1/utilities/lookup?lon=-97.74&lat=30.27" -H "X-API-Key: axiom-local-dev-key"
Every geo endpoint accepts ?format=geojson (default, for the ArcGIS JS
API's GeoJSONLayer) or ?format=esri (Esri JSON FeatureSet, for
ArcGIS REST-based / Runtime SDK clients).
Scaling out
- Locally / small deployments: add more
apiNservices indocker-compose.ymland list them innginx/nginx.conf'supstreamblock. - Cloud: put the API container behind an autoscaling group (ECS Fargate service, Azure Container Apps, or a Kubernetes Deployment + HPA) fronted by the cloud's own load balancer (ALB/NLB, Azure Load Balancer, or a k8s Service). nginx can either stay as an internal LB layer inside the cluster or be replaced entirely by the cloud LB — the API containers themselves don't change either way, because they're stateless (auth is JWT/API-key, no server-side sessions).
- Database: point
AXIOM_DATABASE_URLat Amazon Aurora PostgreSQL (Serverless v2 for spiky trading-hours load) or Azure Database for PostgreSQL — Flexible Server, both with the PostGIS extension enabled. Read replicas can take the read-heavy/geo/*and/assetstraffic.
Plugging into an API Gateway
The service exposes a full OpenAPI 3 spec at /openapi.json:
- AWS API Gateway: import
/openapi.jsondirectly (HTTP API or REST API import-from-OpenAPI), point the integration at the nginx/ALB endpoint, and let API Gateway handle Cognito/IAM auth, throttling, and usage plans in front of it. - Azure APIM: import the same OpenAPI spec as an API definition; APIM handles subscription keys, quotas, and policies in front of the Container Apps/AKS-hosted service.
- Kong: see
gateway/kong.example.yamlfor a working declarative config (key-auth,rate-limiting,corsplugins in front of the nginx service).
In every case, the gateway can either validate auth itself and forward a
trusted identity via the X-Axiom-Gateway-Identity header (paired with a
shared secret — see app/core/security.py), or simply pass through a
bearer JWT/API key for the service to validate itself.
Notes on the domain model
- Assets are deliberately generic. One
assetstable, discriminated byasset_type(solar_farm,well_pad,premise,pipeline_segment,service_territory, ...), with a single PostGISGEOMETRYcolumn that can hold a point, line, or polygon depending on what that asset actually is, plus aJSONB attributescolumn for type-specific fields. New asset types never require a migration. - A "premise" is just an asset (
asset_type='premise') rather than a separate table, so usage records and enrollments — which reference a premise — automatically work for any point asset without special-casing. - AXIOM Exchange transactions optionally reference an
asset_id, so a nomination/trade/settlement can be reconciled against the physical generation asset or delivery point it corresponds to. - 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.
- Saved queries live in a separate database from parcel inventory — different retention, different write profile, independent blast radius.
- This is a working scaffold, not a finished production system: add
Alembic migrations for schema changes going forward (currently the app
autocreates tables in
AXIOM_ENVIRONMENT=localonly), wire up real authentication/authorization scopes per route, and add integration tests before handling real trading volume or customer data.