Files
zeekayandHanzo Dev dd55a53dcd Native CI/CD, and the CoinGecko tier stops being a guess
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]>
2026-08-06 19:30:16 -07:00

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&currency=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, on runs-on: [lux-build-linux-amd64]. Never a hanzo-* pool; cross-org jobs queue silently and cancel at 24h.
  • release.yml — on a v* tag, builds and pushes ghcr.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.
  • /health is 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.