The repo built on GitHub Actions and deployed from hand-applied k8s/ manifests.
Both are retired: .hanzo/workflows/{ci,release}.yml run on lux-build-linux-amd64
and push ghcr.io/luxfi/price, and the deployment is declared in luxfi/universe
where CD reconciles it. Deleting .github/workflows is not only about GitHub —
this forge's WORKFLOW_DIRS includes it, so those files were firing here too.
THE TIER IS NOW DECLARED, NEVER INFERRED. NewPriceCache tried to detect Pro vs
Demo from the key's shape and could not: both are "CG-" plus alphanumeric. So the
branch assigned the demo host on BOTH sides, coingeckoProURL was referenced
nowhere in the program, and every request went out under x-cg-demo-api-key. A Pro
key was being throttled at the free tier with nothing in the logs to say so,
because the demo host answers a Pro key perfectly well — just slower. Host and
header now derive from one COINGECKO_TIER flag so they cannot drift, and an
unrecognised value falls back to demo, which is the safe wrong answer: a Pro key
on demo is throttled, a demo key against Pro is refused.
THE CACHE WAS ONE HOUR. That is a display-ticker number and a dangerous one the
moment a price sizes an amount of money — an hour of BTC is routinely a few
percent, and that gap is slippage somebody eats. Default is one minute,
PRICE_CACHE_TTL overrides, and a malformed or non-positive value REFUSES TO
START rather than silently defaulting: "0s" means no cache at all and burns the
upstream quota in minutes.
⚠ This cache must never price a settlement. It answers "what is BTC worth
roughly, now" for display and for sizing a quote. It does not answer "what did
this deposit realise" — that question's correct answer is the executed fill, and
no cache is it. Stated where someone would otherwise reach for it.
The repo had zero tests. It has three, and both the one-hour TTL and the
demo-host-on-the-pro-branch fail them.
Co-authored-by: Hanzo Dev <[email protected]>
4.1 KiB
Lux Pricing API
USD price feed. Go module github.com/luxfi/price, image ghcr.io/luxfi/price,
served at https://price.lux.cloud.
What it actually is today
main.go is the whole service: an in-process map cache (1h TTL, RWMutex) in
front of CoinGecko only, on four routes.
| Route | Returns |
|---|---|
GET /health |
{"status":"ok","time":...} — liveness only; says nothing about upstream |
GET /price/{id}?currency=usd |
one token |
GET /prices?ids=a,b¤cy=usd |
many |
GET /simple/price?ids=a&vs_currencies=usd |
CoinGecko-compatible shape |
pkg/source holds a Source interface with CoinGecko, Coinbase and Kraken
implementations and the seed ID→symbol / ID→pair maps. Nothing imports it.
It is written and unwired — main.go talks to CoinGecko directly and knows
nothing about pkg/source. Multi-source aggregation is not live; do not
describe the service as aggregating three venues until the wiring lands.
Coinbase and Kraken need no credentials — api.coinbase.com/v2 and
api.kraken.com/0/public are unauthenticated. The only secret this service has
ever needed is a CoinGecko key.
⚠ The CoinGecko key is used as a DEMO key, never a Pro key. coingeckoProURL
is declared and referenced nowhere; NewPriceCache sets baseURL to the demo
host on both branches of its if, and the key is sent as x-cg-demo-api-key
(main.go:252, 291). Provisioning a Pro key will not reach the Pro tier — it
will be sent with the demo header to the demo host. Either provision a Demo key
or fix the client first. The key is optional: unset, the service logs
COINGECKO_API_KEY unset — using demo CoinGecko endpoint with public rate limits and keeps serving.
Build and CI/CD
Native only. .hanzo/workflows/{ci,release}.yml on git.hanzo.ai:
- ci.yml — gofmt +
go vet,go test,CGO_ENABLED=0 go build, onruns-on: [lux-build-linux-amd64]. Never ahanzo-*pool; cross-org jobs queue silently and cancel at 24h. - release.yml — on a
v*tag, builds and pushesghcr.io/luxfi/price:vX.Y.Z(linux/amd64 only — DOKS has no arm64). Refuses to overwrite an existing tag, and re-checks that the pushed tag resolves before going green.
ghcr auth is GHCR_USER / GHCR_TOKEN, org Actions secrets on luxfi. The
instance GITHUB_TOKEN is a Hanzo Git token with no rights at ghcr.io.
⚠ This repo is a pull MIRROR on the forge (mirror=true, upstream
github.com/luxfi/price) on an 8h interval. Push to github.com; the
forge follows. A push to the forge copy is overwritten by the next sync — a
silent no-op. To pick up a push sooner than 8h, trigger
POST /api/v1/repos/luxfi/price/mirror-sync on the forge.
⚠ The forge is configured WORKFLOW_DIRS = .hanzo/workflows, .github/workflows
and reads both. .github/workflows/ is deleted here — its deploy.yml
hand-applied k8s/*.yaml and minted a plaintext Secret from a GitHub Actions
value. Do not reintroduce that directory.
The legacy ghcr.io/luxfi/pricing package (note the name) still exists and
still carries a :latest. It is the old name under the old policy. Nothing
should pull it.
Deployment
Declared in luxfi/universe at deploy/lux-cloud/price.yaml — the directory IS
the namespace, and the file generates the CD Application lux-lux-cloud-price
against lux-k8s. There are no raw manifests in this repo on purpose: two
declarations of one service is how drift starts.
Shipping a build is an image.tag edit in that file. Never kubectl patch from
CI — CD reverts it ~90s later, and it fails in the worst way: the patch applies,
the pod rolls, and the old tag quietly comes back.
Secrets are KMS references, never values: project lux, env prod, path
/price, key COINGECKO_API_KEY.
Gaps worth knowing
- Zero test files in the repo. The CI gate runs
go test ./...and it passes because there is nothing to run.pkg/source's pair/symbol mapping is pure, table-driven logic and is the obvious first test. /healthis unconditional. It cannot distinguish "serving cached prices" from "every upstream call is failing", so a readiness probe on it will keep a broken pod in rotation.