NXT STS is a lightweight Spring Boot microservice that generates prepayment tokens compliant with the IEC 62055-41 (STS) standard. It is used in production to power prepaid utility metering workflows and exposes a simple REST API for token generation.
This project is a derivative work of NectarAPI/tokens-service, which is licensed under AGPL-3.0. See NOTICE for full attribution.
Four token types are exposed via POST /token today. The underlying STS library implements many more; see docs/capabilities.md for the full matrix.
API type |
STS class / subclass | Description | REST API |
|---|---|---|---|
TOP_UP_KWH |
0 / 0 | Transfer electricity credit (kWh) | Yes |
CLEAR_CREDIT |
2 / 1 | Clear existing credit on meter | Yes |
CLEAR_TAMPER |
2 / 5 | Clear tamper condition | Yes |
SET_POWER_LIMIT |
2 / 0 | Set maximum power limit | Yes |
TOP_UP is still accepted as a deprecated wire alias for TOP_UP_KWH (same path) for older
callers. Prefer TOP_UP_KWH for new integrations.
Tokens are generated using the Standard Transfer Algorithm (STA / EA07) via the Bouncy Castle cryptographic library.
To add a new token type, see Adding a token type in CONTRIBUTING.md.
HTTP client
│
▼
POST /token ──► TokenController
│
▼ dispatches to matching strategy
TokenStrategy (interface)
│
├── TransferElectricityCreditStrategy (TOP_UP_KWH)
├── ClearCreditStrategy (CLEAR_CREDIT)
├── ClearTamperStrategy (CLEAR_TAMPER)
└── SetMaximumPowerLimitStrategy (SET_POWER_LIMIT)
│
▼
STS domain objects + nativetoken generators
│
▼
BouncyCastle STA / EA07
│
▼
20-digit IEC 62055-41 token string
Adding a new token type is a single-file change — create one TokenStrategy implementation and Spring picks it up automatically.
The repository is a single Maven module today, but the code is already split so the STS engine can be extracted later without a redesign:
| Layer | Packages | Allowed dependencies |
|---|---|---|
Core (future sts-core) |
co.nxtgrid.token.* |
Java, BouncyCastle, Joda-Time only — no Spring |
| Wrapper (this service) | co.nxtgrid.api.*, co.nxtgrid.strategy.*, StsApplication |
Spring Boot, validation, OpenAPI, etc. |
- Domain objects and
nativetokengenerators live undertoken/and have no Spring imports. TokenStrategyclasses live in the wrapper: they translate HTTP DTOs into domain calls. They are not part of the future library artifact.- Embedding STS generation in another JVM process means depending on core (
token/packages / generators), not onTokenStrategyor the REST DTOs.
See ADR-001 decision 7 for the full
constraint. Publishing a standalone sts-core artifact is deferred — see Roadmap.
Key dependencies
| Library | Purpose |
|---|---|
| Spring Boot 3.4 | HTTP server and dependency injection |
BouncyCastle bcprov-jdk15on:1.70 |
STS cryptographic algorithms (STA/EA07). Legacy Maven coordinate — planned upgrade to bcprov-jdk18on (see Roadmap) |
| Joda-Time 2.13 | IEC 62055-41 date/time handling |
| springdoc-openapi | Interactive OpenAPI / Swagger UI |
The following are deferred until there is a concrete need:
| Next step | What it unlocks | When to start |
|---|---|---|
Extract sts-core |
Publish the Spring-free STA engine as a Maven artifact for embedding in other JVM services | A concrete in-process consumer exists |
Conformance vectors (test-vectors.json) |
Language-neutral golden tokens shared by ports and regression suites | Part of / prerequisite for extraction and ports |
| Multi-language ports | TypeScript, PHP, Python (etc.) implementations validated against the same vectors | After conformance vectors exist |
POST /decode / HSM |
Token decode API or hardware key storage | Separate product request; not required for library extraction |
| Upgrade BouncyCastle | Move from bcprov-jdk15on:1.70 to bcprov-jdk18on (supported artifact line) |
Maintenance / security hygiene; re-run ./mvnw verify and confirm token vectors unchanged |
Until sts-core is published, use this service over HTTP, or call the co.nxtgrid.token.*
generators in-process from a checkout of this repository.
- Java 17+ (for local Maven builds; not required if you only pull the container image)
- Docker (optional, for the container path below)
No local Maven installation is required — the repository includes the Maven wrapper (mvnw).
Deploy the service, confirm it is up, then generate a token.
1. Run (pick one):
# Released image from GHCR
docker run --rm -p 8080:8080 ghcr.io/nxtgrid/nxt-sts:latest
# Or the same image via Compose (this repo's docker-compose.yml)
docker compose up
# Or build and run from this repo
docker build -t nxt-sts . && docker run --rm -p 8080:8080 nxt-sts
# Or run from source (Java 17+)
./mvnw spring-boot:run2. Health check:
curl -s http://localhost:8080/actuator/health
# {"status":"UP"}3. Generate a token (replace the decoder key with a real 16-hex-char meter key):
curl -X POST http://localhost:8080/token \
-H "Content-Type: application/json" \
-d '{
"type": "TOP_UP_KWH",
"issueDate": "2024-03-15T10:30:00",
"randomNumber": 3,
"decoderKey": "XXXXXXXXXXXXXXXX",
"kwh": 0.5
}'Interactive API explorer: http://localhost:8080/swagger.
More detail: Docker, Running, API Reference.
Production: Git autodeploy, health probe, and same-app wiring: docs/deployment/. Compose sidecar: Sidecar / Compose.
./mvnw clean package -DskipTestsThe build produces target/nxt-sts-*.jar. The target/ directory is git-ignored; build artifacts are never committed.
Run all tests:
./mvnw testFull build with tests (recommended before committing):
./mvnw verifyRun a single test class:
./mvnw test -Dtest=TokenStrategyIntegrationTestRun a single test method:
./mvnw test -Dtest=TokenControllerValidationTest#rejectsMissingRandomNumberTests live under src/test/java/co/nxtgrid/ and cover STS token vectors, input validation, and the root service index.
During development:
./mvnw spring-boot:runTo run the packaged JAR (final check before deployment):
./mvnw clean package
java -jar target/nxt-sts-*.jarThe service starts on port 8080 by default. Override with:
java -jar target/nxt-sts-*.jar --server.port=8084The Dockerfile is a multi-stage build — no local Maven or pre-built JAR is required.
# Build the image (builds the JAR inside the container)
docker build -t nxt-sts .
# Run the container
docker run -p 8080:8080 nxt-stsTo pass configuration at runtime:
docker run -p 8080:8080 \
-e SERVER_PORT=8080 \
nxt-stsWhen using a custom port, set SERVER_PORT and map the same host port:
docker run -p 9090:9090 \
-e SERVER_PORT=9090 \
nxt-stsHealth checks
The image includes a Docker
HEALTHCHECKthat probes/actuator/healthevery 30 seconds on the port given bySERVER_PORT(default 8080). Composedepends_on: service_healthyuses that. Disable it for plain Docker if the host probes instead:docker run --no-healthcheck ...PaaS (DigitalOcean App Platform, etc.) ignores the image
HEALTHCHECK. Set the platform HTTP probe in the deployment guides.
docker-compose.yml is a regular Compose file: docker compose up from
this repo starts STS alone. The same nxt-sts service is the fragment to copy into another
stack (such as nxt-device-messaging, or any other caller).
On the Compose network the caller uses http://nxt-sts:8080. Drop ports in the copy if
STS should not be published on the host. Pin image to a release tag in production.
# In your stack — copy from this repo's docker-compose.yml, then typically:
services:
nxt-sts:
image: ghcr.io/nxtgrid/nxt-sts:vX.Y.Z # pin a release tag; do not copy `build`
restart: unless-stopped
# no `ports:` — stay on the Compose network only
your-app:
depends_on:
nxt-sts:
condition: service_healthy # uses the image HEALTHCHECK
environment:
NXT_STS_URL: http://nxt-sts:8080 # or whatever your app uses for the STS base URLEvery push to main and every pull request runs ./mvnw verify via GitHub Actions (build.yml).
Tagged releases (e.g. v1.2.0) automatically publish a multi-arch Docker image
(linux/amd64 and linux/arm64) to the GitHub Container Registry:
ghcr.io/nxtgrid/nxt-sts:v1.2.0
ghcr.io/nxtgrid/nxt-sts:latest
Pull and run a released image (Apple Silicon and x86 hosts pick the matching arch):
docker run -p 8080:8080 ghcr.io/nxtgrid/nxt-sts:latestThe service is self-describing. After starting it locally:
| Endpoint | Description |
|---|---|
GET / |
JSON service index — lists all endpoints and links |
GET /swagger |
Interactive Swagger UI (preferred reference for integrators) |
GET /v3/api-docs |
Machine-readable OpenAPI JSON |
GET /actuator/health |
Health check — returns {"status":"UP"} |
Generates a prepayment token.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | Token type: TOP_UP_KWH, CLEAR_CREDIT, CLEAR_TAMPER, SET_POWER_LIMIT (deprecated alias: TOP_UP → TOP_UP_KWH) |
issueDate |
string |
Yes | ISO 8601 datetime (see note below) |
randomNumber |
integer |
Yes | STS 4-bit RND field — must be 0–15 (see note below) |
decoderKey |
string |
Yes | Meter decoder key as a hexadecimal string (16 hex chars = 8 bytes) |
kwh |
number |
For TOP_UP_KWH |
Amount of electricity credit in kWh (also required when using deprecated TOP_UP; see quantization note below) |
powerLimit |
integer |
For SET_POWER_LIMIT |
Maximum power limit (see note below) |
randomNumber— STS protocol constraint (and same-minute uniqueness)This field maps directly to the 4-bit RND field in the IEC 62055-41 token structure. The protocol defines it as a 4-bit value, so only 0–15 is valid — this is not an arbitrary API limit. Values outside this range will be rejected with HTTP 400.
The STS token identifier (TID) is minute-granular: only the UTC date and minute of
issueDateenter the TID (seconds and sub-seconds are ignored). So10:30:00and10:30:59produce the same TID. With identicaldecoderKey, amount, and other fields, two requests in the same wall-clock minute produce a byte-identical token unlessrandomNumberdiffers.Callers should therefore track the last-used RND per meter and advance it for each new issue (especially when vending more than once in the same minute). Meters also reject a token that reuses the same
randomNumberas the most recently accepted token (anti-replay). A value of 0 is valid but should not be reused immediately. With only 16 possible RND values, high-frequency same-minute vending on one meter will exhaust the space unless the caller waits for the next minute or otherwise avoids collisions.See the full schema in the Swagger UI.
issueDate— wall-clock semantics (UTC)Accepts ISO 8601 forms such as
"2024-03-15T10:30:00","2026-07-07T10:12:54.289", or"2026-07-07T10:12:54.289Z". Optional fractional seconds and UTC/offset suffixes are allowed. Any time-zone offset is ignored; the date and time fields are interpreted as UTC for TID calculation, independent of the server's timezone.TID uses minute resolution only — changing seconds within the same minute does not change the token. For uniqueness of consecutive issues, vary
randomNumber(see above).
kwh— amount quantization (0.1 kWh steps)The STS transfer-amount field does not store an arbitrary floating-point kWh value. Credit is encoded in tenths of a kWh (0.1 kWh steps). Before packing into the token, this service maps the request
kwhonto that grid as follows (inherited from NectarAPI/tokens-service; unchanged in NXT STS):
Requested kwhMapping Effective credit on the token < 1ceil to the next 0.1 kWh e.g. 0.01→ 0.1,0.11→ 0.2,0.5→ 0.5≥ 1truncate toward zero to a 0.1 kWh step e.g. 1.19→ 1.1,1.99→ 1.9Very small top-ups therefore cannot encode as zero (
0.01becomes0.1). Larger amounts drop any leftover fraction of a tenth rather than rounding up.Recommendation for callers: send
kwhvalues that are already multiples of0.1so the mapping is exact, and treat billing/ledger amounts as that quantized value (not an unrounded intermediate float). Changing this rule would alter token output for the same inputs and break compatibility with existing meters and systems.Maximum:
kwhmust not exceed 1820162.4 (the STS 16-bit amount field maximum). Larger values are rejected with HTTP 400.
powerLimit— STS maximum
SET_POWER_LIMITuses the same 16-bit STS amount encoding as credit tokens, but the request value is not scaled by 10 (unlikekwh).powerLimitmust be an integer from 0 to 18201624. Larger values are rejected with HTTP 400.
Example — TOP_UP_KWH
curl -X POST http://localhost:8080/token \
-H "Content-Type: application/json" \
-d '{
"type": "TOP_UP_KWH",
"issueDate": "2024-03-15T10:30:00",
"randomNumber": 3,
"decoderKey": "XXXXXXXXXXXXXXXX",
"kwh": 0.5
}'Response
{
"token": "12345678901234567890"
}Error responses
| HTTP | Cause |
|---|---|
400 |
Missing required field, invalid type, randomNumber out of range (0–15), malformed issueDate |
500 |
Unexpected internal error (check logs) |
Security note: The decoder key is a sensitive credential specific to each meter. It must be transmitted only over encrypted channels (HTTPS) and never logged or stored in plaintext.
src/main/resources/application.properties is committed with safe defaults. All settings can be overridden via environment variables (Spring Boot relaxed binding) or JVM arguments.
| Property | Env variable | Default | Description |
|---|---|---|---|
server.port |
SERVER_PORT |
8080 |
HTTP listen port |
server.error.whitelabel.enabled |
— | false |
Returns JSON errors instead of HTML pages |
management.endpoints.web.exposure.include |
— | health,info |
Actuator endpoints exposed |
management.endpoint.health.show-details |
— | never |
Hides internals from health response |
spring.application.name |
SPRING_APPLICATION_NAME |
nxt-sts |
Service name |
springdoc.swagger-ui.path |
— | /swagger |
Swagger UI path |
springdoc.api-docs.path |
— | /v3/api-docs |
OpenAPI JSON path |
For production deployments pass all secrets (decoder keys, credentials) as environment variables or secret volumes — never hard-code them in source.
See CONTRIBUTING.md.
See AUTHORS.md.
Copyright (C) 2024–2026 Bobby Bol, Tommaso Girotto.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See LICENSE for the full license text.
This project is a derivative work of NectarAPI/tokens-service (AGPL-3.0). See NOTICE for attribution details.