A typed Python client for the Pyth Network Hermes price-oracle API. Sync and async clients, Pydantic v2 models, Server-Sent-Events price streaming with auto-reconnect, and a Decimal price helper.
- Sync (
HermesClient) and async (AsyncHermesClient) APIs overhttpx - SSE streaming with reconnect + backoff
- Graceful 429 rate-limit handling (retries that respect the 60s window)
- Configurable
base_url(production, beta, or paid providers) and optional API key from day one mypy --strictclean, fully type-hinted, shipspy.typed
pip install pyth-hermes
# with the optional pandas helper:
pip install "pyth-hermes[pandas]"from pyth_hermes import HermesClient
client = HermesClient()
feed_id = client.get_feed_id("Crypto.BTC/USD") # exact-symbol lookup
print(client.get_price_decimal(feed_id)) # -> Decimal("63952.82...")import asyncio
from pyth_hermes import AsyncHermesClient
BTC = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
async def main():
async with AsyncHermesClient() as client:
async for update in client.stream_prices([BTC]):
print(update.parsed[0].to_decimal())
asyncio.run(main())resp = client.get_price_at(1718900000, [BTC]) # unix timestamp
print(resp.parsed[0].to_decimal())from pyth_hermes.pandas import updates_to_dataframe
df = updates_to_dataframe([client.get_latest_price([BTC])])Pyth returns integer prices plus an exponent. The real value is price * 10**expo, computed exactly as a Decimal:
from pyth_hermes import price_to_decimal
price_to_decimal(6395282153102, -8) # Decimal("63952.82153102")RpcPrice.to_decimal() and ParsedPriceUpdate.to_decimal() are convenience wrappers.
/v2/price_feeds?query=btc returns deprecated / variant feeds (e.g. MBTC, XBTC) before the canonical one and matches substrings. get_feed_id() therefore matches on exact attributes.symbol:
client.get_feed_id("Crypto.BTC/USD")
# -> "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"Today the public endpoint needs no API key. From 2026-07-31 an API key becomes mandatory. This client accepts one from day one — pass it now to be ready:
client = HermesClient(api_key="YOUR_KEY") # default: Authorization: Bearer YOUR_KEYThe exact header is not finalized publicly, so both the header name and scheme are configurable:
HermesClient(api_key="KEY", api_key_header="X-Api-Key", api_key_scheme="") # -> X-Api-Key: KEYfrom pyth_hermes import HermesClient
HermesClient() # production: https://hermes.pyth.network
HermesClient(base_url="https://hermes-beta.pyth.network") # beta
HermesClient(base_url="https://your-paid-provider.example") # Triton / P2P / extrnode / LiquifyYou may also inject your own preconfigured httpx.Client / httpx.AsyncClient via client=... (e.g. for custom transports, proxies, or connection pools). In that case the request host is taken from your client, so set base_url on the client itself — passing both base_url= and client= raises a UserWarning because the constructor's base_url would be a no-op. The api_key is still applied per-request, so it works with an injected client.
import httpx
from pyth_hermes import HermesClient
http = httpx.Client(base_url="https://your-paid-provider.example", proxy="http://localhost:8080")
client = HermesClient(api_key="KEY", client=http) # base_url comes from `http`The public endpoint allows 10 requests / 10 seconds per IP. Exceeding it returns HTTP 429 for the next 60 seconds. The client retries 429 and 5xx responses with exponential backoff + jitter, honoring any Retry-After header and never exceeding the 60s rate-limit window per delay. Tune via max_retries, backoff_base, backoff_cap.
The TWAP endpoints (/v2/updates/twap/...) are intentionally omitted — the API returns HTTP 400 "deprecated and no longer available".
pip install -e ".[dev]"
pytest # unit tests (no network)
pytest -m integration # live smoke tests against production
mypy
ruff check .MIT