feat: merge coingecko and facilitator changes#60
Merged
Conversation
Replaces the hardcoded mock price (Decimal("1")) with a real CoinGecko
price feed that runs as a background daemon thread.
Key behaviour:
- Fetches OPG/USD price from CoinGecko /simple/token_price/base at startup
and every 5 minutes thereafter (well within free-tier rate limits)
- Retries up to 3 times per refresh cycle with 10s delay between attempts;
exits retry loop immediately on 429 (rate-limited) to avoid hammering
- Retains the last known good price on exhausted retries so live traffic
is not disrupted by a transient CoinGecko outage
- Logs a WARNING when the cached price is older than 2× the refresh
interval (background loop may be stuck)
- Tracks last_success, last_error, consecutive_failures, total_fetches,
total_errors via get_status() / get_price_feed_status()
- get_price() raises ValueError when no price has ever been fetched,
which propagates through dynamic_session_cost_calculator and the
existing strict _resolve_session_request_cost monkey-patch to return
HTTP 500 rather than silently charging an incorrect amount
Also adds:
- 29 unit tests (all mocked, no network required) covering the fetch
helper, retry logic, rate-limit handling, stale warning, stats, and
module-level singleton functions
- 4 integration tests that hit the live CoinGecko API; OPG-specific
tests skip gracefully until the token is fully indexed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves opg_price_feed.py into a dedicated package matching the layout of
the existing tee_gateway/heartbeat/ package:
tee_gateway/price_feed/
__init__.py — re-exports public API (OPGPriceFeed, PriceFeedConfig,
start_price_feed, get_opg_price_usd, get_price_feed_status)
config.py — all constants (COINGECKO_BASE_URL, COINGECKO_PLATFORM,
FETCH_TIMEOUT, refresh/retry defaults, stale threshold)
plus the PriceFeedConfig frozen dataclass
feed.py — OPGPriceFeed class, fetch_opg_price(), singleton helpers
Also renames the test files to match:
test_opg_price_feed.py -> test_price_feed.py
test_opg_price_feed_integration.py -> test_price_feed_integration.py
No behaviour changes — all 29 unit tests still pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lator Replace module-level singleton in price_feed with a factory/closure pattern. make_cost_calculator(price_feed) in util.py binds an OPGPriceFeed instance explicitly, eliminating hidden global state. Tests updated: TestModuleLevelFunctions removed, TestMakeCostCalculator added (10 cases). Also fix missing Any import in feed.py and unused PriceFeedConfig import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_cost + named function Remove the factory/closure pattern and _PriceSource Protocol. util.py now exports calculate_session_cost(context, get_price) — a plain function that accepts a Callable[[], Decimal]. __main__.py wires it via a named _session_cost_calculator function that passes _price_feed.get_price directly. Tests updated to match the new signature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace dynamic_session_cost_calculator import with calculate_session_cost and pass _get_price (OPG=$1.00) to all call sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- start() is now non-blocking: initial fetch runs inside the background thread instead of blocking the caller - start() is idempotent: duplicate calls are a no-op if thread is alive - Clear last_error on successful refresh so /health reflects recovery - Gate integration tests behind RUN_INTEGRATION_TESTS env var to prevent real CoinGecko calls in CI by default - Fix stale docstring references to make_cost_calculator → calculate_session_cost Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Rename test classes and update comment/error message in __main__.py to match the current calculate_session_cost / _session_cost_calculator names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the _strict_resolve_session_request_cost monkey-patch (which was patching a method not called in the upto session flow) with a proper before_request hook that rejects inference requests early if pricing would fail: - 503 if the OPG price feed has no valid price yet - 400 if the requested model is not in the registry _session_cost_calculator now logs CRITICAL with full traceback on any post-inference cost failure (e.g. missing usage field) so uncharged requests are never silently missed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hread safety) - Update start() docstring: replace monkey-patch reference with pre-inference gate - Make fetch_opg_price() validate response.json() is a dict before calling .get(), raising ValueError instead of AttributeError on unexpected CoinGecko response shapes - Make start() idempotency check thread-safe under _lock - Clarify CRITICAL log: provider error, x402 swallows exception so client is not charged - Update calculate_session_cost docstring: replace monkey-patch reference with pre-inference gate and CRITICAL log behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before the TGE cutover (2026-04-21 12:30 UTC) get_price() returns a fixed $0.10 fallback so inference requests can be priced before OPG is listed on CoinGecko. After the cutover the live cached price is used. Also adds a guard in fetch_opg_price() rejecting non-positive or non-finite prices from the API response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_price() now raises ValueError if the cached price is older than 4 hours, preventing billing on a price that is too outdated. A warning is still logged at the existing 2 × refresh_interval threshold (~10 min) as an early signal. The pre-inference gate in __main__.py will surface this as a 503. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the x402 payment middleware (facilitator, server, routes, session store) was created at module load using a hardcoded FACILITATOR_URL, which meant the URL couldn't be injected at runtime and the middleware was never actually wired into Flask's request chain (application.run() bypassed _payment_mw entirely). Changes: - Move all x402 setup into _init_payment_middleware(facilitator_url), called once from set_provider_keys(). Uses application.wsgi_app = mw (the standard Flask WSGI middleware pattern) so all requests flow through payment checking after injection. - Accept a single `facilitator_url` field in POST /v1/keys, used for both x402 payment verification and the heartbeat relay. Removes the separate `heartbeat_facilitator_url` field. - Fallback chain: injection payload → FACILITATOR_URL env var → definitions.py hardcoded default. - Update run-enclave.sh: HEARTBEAT_FACILITATOR_URL → FACILITATOR_URL, heartbeat_facilitator_url JSON key → facilitator_url. - Update definitions.py comment to document the full precedence chain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FACILITATOR_URL in definitions.py already reads from os.getenv, so the middle step was always superseded by the final fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PaymentMiddleware.__init__ does app.wsgi_app internally, so it needs the full Flask application object. Passing application.wsgi_app (a plain function) caused AttributeError. payment_middleware captures the inner wsgi_app by value at creation time, so setting application.wsgi_app = mw afterwards is safe and does not create a circular reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PaymentMiddleware.__init__ already does app.wsgi_app = self._wsgi_middleware internally. Our manual application.wsgi_app = mw was overwriting that bound method with the bare PaymentMiddleware instance (which has no __call__), causing TypeError on every request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add comment explaining why payment_middleware return value is discarded (PaymentMiddleware self-wires via app.wsgi_app in __init__) - Fix run-enclave.sh heartbeat status message: condition on HEARTBEAT_CONTRACT_ADDRESS alone, with a nested check for FACILITATOR_URL to distinguish injected vs enclave-default URL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
adambalogh
approved these changes
Apr 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge these two changes into main
Also revert max cap PR