We're building a prediction game on Solana where every shot settles on Pyth, and we ended up doing the account validation by hand rather than through the SDK. Sharing what we found, plus four questions we couldn't answer from the docs. Why by hand. Our fi

We’re building a prediction game on Solana where every shot settles on Pyth, and we ended up doing

the account validation by hand rather than through the SDK. Sharing what we found, plus four

questions we couldn’t answer from the docs.

Why by hand. Our first version declared the price account as an Anchor Account<'info, PriceUpdateV2> with #[account] on the inlined struct. That compiles and looks right, and it

fails on-chain with:

Code

Obvious in hindsight — #[account] makes Anchor demand our program own the account — but the

error names the correct Pyth receiver as “Left”, which reads like the Pyth account is wrong rather

than our declaration. Posting it here mostly so it’s searchable for the next person.

The fix was UncheckedAccount plus explicit checks, which also let us drop the SDK dependency

entirely (the whole program is 288 lines, zero external crates):

Rust

Plus a staleness bound at seal (price must be < 60s old) and a publish-time bound at settle.

The settlement problem. Settlement is permissionless — anyone can settle any expired shot — so

whoever cranks it must not get to choose the outcome. v0 required

expiry <= publish_time <= expiry + 60, which bounds the cranker but still leaves a choice among

updates inside the window.

v1 removes the choice using prev_publish_time:

Code

Exactly one update in existence satisfies that — the first price published at or after expiry — so

there is nothing left to pick. We keep a relaxed fallback after an hour so a shot can’t be stranded

if nobody cranks it in time, and the account records which rule was used.

One field observation. On devnet the sponsored SOL/USD account

(7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE, shard 0) publishes in bursts roughly every 15–20

seconds and runs a few seconds behind the chain clock. Our program correctly refused to settle for

about a minute waiting for a publish time at or after expiry — good to know if you’re testing

timing-sensitive logic against devnet and wondering why nothing settles.

Questions:

Is prev_publish_time safe to rely on for a first-crossing proof? Can it ever be zero,

equal to publish_time, or otherwise non-monotonic — for example on a feed’s first update after

an outage, or across a shard?

For settling against a specific past timestamp, is the intended pattern to post the crossing

update from Hermes via post_price_update, rather than reading whatever the sponsored account

currently holds? Ours works both ways today, but we’d rather follow the intended path.

Is there a documented heartbeat / max staleness for sponsored feed accounts, and does it

differ between mainnet and devnet? We’re choosing a staleness bound and would rather derive it

from your guarantees than from our own measurements.

Is shard 0 canonical for sponsored feeds, and is there a case where a consumer should prefer

another shard?

Program is on devnet and the source is public if anyone wants to pick holes in the validation —

particularly the settlement rule, which is the part I’d attack first:

GitHub - 3esign/ratchetx · GitHub (onchain/ratchet_seal_lib.rs)

Thanks for the pull oracle — the fact that a 288-line program with no dependencies can verify a

price properly is the whole reason this design works.

For a new Solana deployment, please use upgraded Pyth Core with the Pro-compatible receiver rather than the legacy receiver or push-oracle addresses.

The account-validation approach is sound, but we recommend importing PriceUpdateV2 from the latest pyth-solana-receiver-sdk and enabling its pro-compatible feature instead of duplicating the account layout manually.

For settlement:

  • Use an ephemeral PriceUpdateV2 account populated from the upgraded Hermes endpoint.
  • For a deterministic first-crossing rule, accept only prev_publish_time < expiry <= publish_time.
  • Treat prev_publish_time == publish_time or a missing crossing update as non-strict/unresolvable; do not let a late cranker choose an arbitrary price if fairness is required.
  • Shard 0 is the default sponsored shard, but use the upgraded feed-account addresses and program IDs from the upgrade documentation.

The sponsored-feed heartbeat is an update target, not a hard freshness guarantee, so keep the staleness bound as an application-level policy.

Follow the upgraded-Core Solana setup here:
Solana | Pyth Developer Hub

Cool thnx will update asap

Q1 — prev_publish_time for first-crossing. You told us to treat
prev_publish_time == publish_time as invalid, and not to let a settler pick an arbitrary
price when fairness is the point. Both shipped. The rule is now strict and has no escape
hatch:
Rust
We deleted the permissive one-hour fallback we had described in the original post. It was
there so a shot could never be stranded if nobody cranked it, and you were right that this
is the wrong trade: a fallback that lets the settler choose among updates is a fairness hole
that opens exactly when someone has a reason to use it. A shot that cannot be settled
correctly should stay open, not settle conveniently. The account still records which rule
was used, but there is now only one rule.
Also fixed from your reply: we were carrying stale program addresses. The validator now
accepts both generations as owners — rec5EK…/pythWSns… and rec2HH…/pyt2F4… — for the
reason you gave, that an account posted before the upgrade must stay readable after it.
Q3/Q4 — staleness and shards. Took the documented 1-minute heartbeat / 0.5% deviation
rather than our own measurements, and stayed on shard 0.
Now the part that changed on us.
Your answer to Q2 was that the intended pattern for settling against a specific past
timestamp is to post the crossing update from Hermes via post_price_update, rather than
reading whatever the sponsored account currently holds. We took that as the target
architecture.
Then the Core upgrade put the Hermes API behind a plan starting at $500/month, with the free
Terminal tier listed as “No Pyth API access.”
So we did the other thing. Our off-chain service now reads the sponsored push accounts
directly over Solana RPC and decodes PriceUpdateV2 itself — no SDK, no key, seven feeds
in two batched getMultipleAccounts calls. Same struct, same publishers, same signatures,
and a Solana account read is not something anyone can meter.
Two things we like better about it than what it replaced. Our website now reads the exact
same account our on-chain program validates — before, the page trusted an HTTP endpoint and
the program trusted an account, and users had to take our word that they agreed. And we can
show each price’s publish age on the page, so a number that claims to be 3 seconds old is
making a checkable claim.
We apply the same bar off-chain that the program applies on-chain: owner must be one of the
four Pyth programs, discriminator must equal sha256(“account:PriceUpdateV2”)[..8],
verification_level must be Full, the feed_id inside the account must match the feed we
asked for, and publish_time must be under 120 seconds old. A core feed failing any of those
falls through to a non-Pyth fallback that the page labels loudly as degraded. An optional feed
failing simply disappears from the board rather than settling a shot on a number we don’t
trust.
New questions
5. Does the Q2 answer still hold, and if so, is permissionless settlement against a past
timestamp now a paid feature? This is the one we’d most like to be wrong about. A
sponsored push account only ever holds the latest update, so an integrator who needs the
first update at or after time T has to source it from somewhere with history. If that
somewhere is Hermes, then any protocol whose settlement rule references a past timestamp —
options expiry, funding windows, auction closes, our shot expiries — now needs a subscription
to be settled permissionlessly by a third party. Is there a free or on-chain path to a
crossing update that we’ve missed? Benchmarks, a public historical endpoint, something in the
receiver program?
6. Will the sponsored Solana feeds stay sponsored, and is there a deprecation policy?
Reading the accounts is free by construction, but someone is paying to push those updates,
and after this upgrade we’d rather ask than assume. Is there a commitment or a notice period
for the sponsored set? A sizeable number of Solana projects are about to make the same move
we just made, and it would be good to know whether we’re all standing on something durable.
7. What happens to feeds marked “Coming soon” in the Pro-compatible column? Four of our
seven — BONK, WIF, JUP, PUMP — are the long tail, and the docs say migration isn’t guaranteed
and to contact you if we depend on a specific feed. Consider this us contacting you. Is
“Coming soon” a roadmap or a warning?
8. Is there a machine-readable source for per-feed heartbeat and deviation parameters?
We hardcode a 120-second staleness bound because it’s two of the documented 60-second
heartbeats, but some feeds run 30s and at least one runs 4 minutes. We’d rather read those
from you than pin a constant that silently becomes wrong.
Source is public if anyone wants to attack the validation — the off-chain decoder is the new
part and the settlement rule is still the part we’d attack first:
github.com/3esign/ratchetx (lib/onchain_px.js, onchain/ratchet_seal_lib_v1.rs)
Thanks again for the Q1 answer specifically. “Don’t let the settler choose” is a one-line
principle that removed a hole we’d have shipped.

Following up on two of the questions above — not the ones specific to us, the two that affect anyone settling against a past timestamp.

Since posting we moved our off-chain settlement onto the sponsored push accounts, and it has been running in production for several days: seven feeds, two batched getMultipleAccounts calls, decoded without the SDK. The strict prev_publish_time rule you steered us to is live and the permissive fallback is gone.

That is exactly why these two matter now.

1. Is settling against a specific past timestamp a paid feature now? A sponsored push account only holds the latest update. Anyone whose settlement rule references a past moment — options expiry, funding windows, auction closes, our shot expiries — needs history, and history is Hermes. If that is the intended answer we will design around it, but a number of Solana protocols are about to hit the same wall and it would be useful to have it stated publicly.

2. Will the sponsored Solana feeds stay sponsored? Reading the accounts is free by construction, but somebody is paying to push them. Is there a commitment, or a notice period? We are building on the assumption that they stay, and would rather be told now if that assumption needs a fallback behind it.

Not asking for a roadmap — only whether these are load-bearing assumptions or not.

A sponsored push account should be treated as the latest on-chain value, not as a historical archive. For a specific expiry timestamp, you can query the public Benchmarks timestamp endpoint:

GET https://benchmarks.pyth.network/v1/updates/price/{timestamp}?ids={feed_id}&encoding=base64&parsed=true

The response includes binary.data, which is the signed Core price-update payload, plus parsed metadata including prev_publish_time. You can submit binary.data through the upgraded Solana receiver and enforce:

prev_publish_time < expiry <= publish_time

If that predicate is not satisfied, the transaction should remain unresolved rather than selecting another update. The receiver itself does not provide an arbitrary historical archive.

The alternative is the Hermes timestamp endpoint, which follows the same first-update-at-or-after-timestamp model. Hermes API access requires a Pyth API key from August 26, 2026 at 16:00 UTC. The free Terminal tier is view-only; the free API access is a trial, while ongoing API access uses paid plans. That makes sustained Hermes retrieval a paid service, but historical settlement is not inherently limited to a paid Hermes subscription while the public Benchmarks route remains available. Benchmarks is rate-limited and should not be treated as an SLA.

For sponsored Solana feeds, the current documentation lists the supported set as sponsored on both mainnet and devnet, with shard 0 shown as the default and feed-specific heartbeat/deviation parameters listed here:

One correction to my previous reply: please don’t treat Benchmarks as a permanently free or supported fallback. The legacy charting endpoints are being sunset, and the remaining timestamp endpoints require API-key authentication.

For this settlement flow, use the upgraded Hermes timestamp endpoint with a Pyth API key:
https://pyth.dourolabs.app/hermes/v2/updates/price/{publish_time}

Request the required feed ID and binary encoding, submit binary.data through the upgraded Core Solana receiver, and enforce:

prev_publish_time < expiry <= publish_time

If settlement may happen after the available Hermes history window, the application should archive the Core-compatible payloads itself or run a keeper that fetches and posts them promptly. A sponsored push account alone cannot reconstruct arbitrary historical updates.

The Pyth Pro /v1/{channel}/price payload is a different format and is not a drop-in replacement for an upgraded Pyth Core receiver.

Thank you — that correction saves us from building on sand. We’re going with a two-layer design: the guaranteed path reads the sponsored push account on-chain via a permissionless “checkpoint” instruction after expiry (the program keeps the earliest qualifying publish_time across all callers, so competing crankers can only improve the answer), with our void-and-refund window as the liveness floor. The keyed Hermes timestamp endpoint + prev_publish_time < expiry <= publish_time predicate becomes an optional precision layer we can enable later, with our own payload archiving per your guidance.

One last question: for the upgraded Core receiver on Solana mainnet — is there a canonical program ID and a recommended way to read publish_time/prev_publish_time from the posted update account CPI-side?

Design doc, public: GitHub - 3esign/ratchetx: A keyless prediction market arcade on Solana — sealed commit-reveal shots settled on Pyth oracle prices, 70% of every stake burned, 0% to the team, no custody. Public proof page, hash-chained log, agent API + MCP server: machines welcome. · GitHub → docs/SETTLEMENT.md