Skip to content

feat(scenarios): add the Pyth Core Compatible receiver price feed template - #33

Open
92Infinitus92 wants to merge 17 commits into
feat/scenarios/override/pyth-pro-supportfrom
feat/scenarios/pyth-pro-receiver
Open

92Infinitus92 wants to merge 17 commits into
feat/scenarios/override/pyth-pro-supportfrom
feat/scenarios/pyth-pro-receiver

Conversation

@92Infinitus92

@92Infinitus92 92Infinitus92 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Adds pyth-core-compatible-price-feed-v2, a template for the PriceUpdateV2 accounts owned by Pyth's Core Compatible receiver (rec2HHDDnjLfj4kE7VyEtFA1HPGQLK33259532cRyHp), as Pyth's Solana contract addresses name it, and keeps the price accounts of both Pyth receivers published on a surfnet.

Some programs, Project 0 among them, read their prices from this receiver rather than the Pyth Core one (rec5EKMG…), so pyth-price-feed-v2 cannot write them. The bundled IDL is the one the receiver publishes on chain, converted to the current Anchor format, with one change made before the conversion: the account names priceUpdateV2 and twapUpdate are capitalized as the program's Rust types are, so the discriminators match the live accounts. The README in pyth/core-compatible/v2 records the source and both sha256 values. The template takes the price account's address; its llm_context explains to take it from the consuming program.

Keep-alive. This PR is stacked on #53 (the owned keep-alive kind and the per-override keepAlive flag); merge #53 first.

  • Both Pyth templates import a keep-alive template over every PriceUpdateV2 account of their receiver. Once an override of either template with keepAlive: true plays, every price account of that receiver the surfnet has loaded gets publish_time and posted_slot from the Clock every slot, also feeds that no override names.
  • This changes pyth-price-feed-v2 for existing scenarios: after such an override plays, the receiver's feeds no longer age. Its old stale-price recipe (pause the clock, set publish_time, time travel) is removed, because keep-alive rewrites publish_time. The llm_context of both templates now gives the stale-price recipe: an override with keepAlive: false on the stale feed and one with keepAlive: true on another feed, both writing the current price.
  • pyth-price-feed-v2's llm_context also says which receiver a protocol reads: Save and Kamino read Pyth Core, Project 0 reads Pyth Core Compatible (its templates come in feat(scenarios): add Project 0 support - state templates #34).

Tests: core_compatible_price_feed_touches_only_price_fields writes a real mainnet price account and checks that only the price fields change. price_feeds_stay_published_through_surfnet_svm checks keep-alive across slots, for a feed loaded later and after a time travel.

Shared with the other open protocol PRs, byte for byte: the MCP params description.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds a pyth-pro-price-feed-v2 template for PriceUpdateV2 accounts owned by the Pyth Pro-compatible receiver (rec2HHDD…). Callers pass the account address directly; the template doesn't derive a PDA. The PR also bundles a copy of the Pyth IDL with only address changed, updates the registry counts and tests, and adds a mainnet integration test behind the integration-tests feature.

The approach works. The integration test is well built: every value it writes differs from the live one, it checks a no-op round trip, and it asserts that every changed byte falls inside a written field.

Issues

  • The PR does more than the description says ("YAML and IDL only"). It also changes the CallSurfnetRpcParams.params schemars description in crates/mcp, and it adds .github/workflows/claude-review.yml. Neither has anything to do with the Pyth template. Consider splitting them into their own PRs, or at least mentioning them in the description.
  • Duplicated IDL (~130 lines). pyth/pro/v2/idl.json is byte-for-byte the v2 IDL apart from address. If the upstream IDL is ever updated, the two copies will silently drift apart. You could load PYTH_V2_IDL_CONTENT, overwrite idl.address with the Pro receiver, and drop the second file.
  • The live test depends on mainnet state. It hard-codes byte offsets that are only valid for VerificationLevel::Full, and it relies on one specific feed account. The asserts fail with clear messages, which helps, but the test can break without any code change. That's acceptable since it's behind the feature flag.

Comment thread crates/core/src/scenarios/registry.rs Outdated

pub const PYTH_V2_IDL_CONTENT: &str = include_str!("./protocols/pyth/v2/idl.json");
pub const PYTH_V2_OVERRIDES_CONTENT: &str = include_str!("./protocols/pyth/v2/overrides.yaml");
pub const PYTH_PRO_V2_IDL_CONTENT: &str = include_str!("./protocols/pyth/pro/v2/idl.json");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This IDL is identical to PYTH_V2_IDL_CONTENT except for address. To avoid keeping two copies in sync, you could parse the v2 IDL and overwrite address with rec2HHDDnjLfj4kE7VyEtFA1HPGQLK33259532cRyHp before calling load_collection.

TIME: leave publish_time, prev_publish_time and posted_slot unset. fetchBeforeUse: true
brings the upstream account's current values when the override plays, so the price is as
fresh as upstream's. A time written into the scenario is fixed when the scenario is created
and ages until it plays. To make a price stale on purpose, follow the TIME FIELDS steps of

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tells the agent to follow the TIME FIELDS steps in another template's llm_context. An agent that only loaded this template won't have those steps, and the two can drift if pyth-price-feed-v2's context gets edited. Consider inlining a short version of the pause → read Clock → timeTravel (ms) steps. The "FIELDS OTHER THAN PRICE" guidance (exponent rescales everything, posted_slot is a slot) is also missing here.

)]
pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MCP schema change has nothing to do with the Pyth Pro template and isn't in the PR description. Consider a separate PR. Also, Number is i64 only, so non-integer values can't be expressed. It's worth saying that explicitly, and mentioning the PublicKey/MintAddress/ProgramId/Null variants, or stating that String is the one to use for pubkeys.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds pyth-pro-price-feed-v2, a template for PriceUpdateV2 accounts owned by the Pyth Pro-compatible receiver (rec2HH…). The bundled IDL is a copy of the Pyth v2 IDL. I diffed the two and the only change is address. The registry loads it under "pyth-pro", and register_builtin_template_idls also registers it with the SVM, so owner-based IDL lookup and the jsonParsed reads that llm_context relies on should work for these accounts. The registry tests (count 70→71, by-protocol, by-tag) are updated consistently, and the new live test is strong: it round-trips the bytes without changes, then checks that only the declared field ranges change.

Overall this looks correct and ready to merge. Minor points:

  • Unrelated file in the diff: .github/workflows/claude-review.yml appears in the base…head diff. If it comes from another branch, rebase or drop it so this PR only contains the Pyth Pro change.
  • Duplicated IDL: pyth/pro/v2/idl.json is a 130-line copy that differs in one field. That's acceptable for now. If more mirrored receivers are added, consider loading the base IDL and overriding address in code, so the copies can't drift apart.
  • MCP params description: it lists every JsonValue variant except Null, which serializes as the plain string "Null". Mentioning that would stop LLM callers from guessing {"Null":null}. Number is also i64 only, so floats can't be expressed. "whole number" hints at this, but stating it outright would be clearer.

description: Uncertainty band around the price, in the same scale as price
- path: price_message.exponent
label: Exponent
description: Decimal exponent, normally -8 and -10 on some feeds. Rescales every price field

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is hard to parse. Suggest: "Decimal exponent, usually -8 (-10 on some feeds). Rescales every price field." It would also help to say that changing the exponent alone reinterprets the price, conf and EMA values, so an LLM doesn't edit only the exponent.

let live = fetch(PRO_SOL_USD_FEED).await;
assert_eq!(live.owner.to_string(), PYTH_PRO_COMPATIBLE_RECEIVER);
// A Partial verification level shifts every offset below by one.
assert_eq!(live.data[40], 1, "verification level should be Full");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hard-coded offsets depend on the live mainnet account being Full. If 7AviUf… is ever posted with Partial verification, the test fails with a message that doesn't make clear it is an upstream data issue rather than a regression. Consider adding to the assertion message that the account's verification level changed upstream and the test needs a different feed or offsets shifted by one.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description lists every variant except Null. Because it is a unit variant, it serializes as the bare string "Null" rather than {"Null":...}, which LLM callers are unlikely to guess. Consider adding "null as "Null"". Number is also i64 only, so it may be worth saying that fractional numbers aren't supported.

@92Infinitus92 92Infinitus92 changed the title feat(scenarios): add the Pyth Pro receiver price feed template feat(scenarios): add the Pyth Core Compatible receiver price feed template Oct 29, 2026
…th-pro-receiver

# Conflicts:
#	crates/core/src/scenarios/registry.rs

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds pyth-core-compatible-price-feed-v2, a template for PriceUpdateV2 accounts owned by the Pyth Core Compatible receiver (rec2HHDD…). It includes:

  • the IDL, converted from the on-chain legacy IDL, with the account names capitalized so the discriminators match the live accounts
  • the overrides YAML, the registry wiring and updated registry counts
  • a unit test and a live mainnet integration test, gated behind integration-tests

Assessment

The approach is sound and low-risk. No Rust logic changes, and the new IDL has its own address, so registering IDLs by owner program does not collide with the existing rec5EKMG… IDL.

The integration test is well built:

  • It checks the verification level byte and the feed id before relying on fixed offsets.
  • It checks that a no-op forge returns the bytes unchanged.
  • It checks that only the expected byte ranges change.
  • It requires a row for every template property, so adding a property without a test row fails.

I checked these and they look correct: -10i64.to_le_bytes()[..4] gives the right bytes for the i32 exponent, and data byte 40 is VerificationLevel::Full.

Notes / nits

  • Unrelated changes: .github/workflows/claude-review.yml and the MCP params description in crates/mcp/src/surfpool/mod.rs have nothing to do with the Pyth template. The PR body explains the MCP text, but not the workflow. Consider landing both separately so the PR history stays focused.
  • Receiver naming: the top-level scenarios README calls rec5EKMG… the "default receiver", while pyth/core-compatible/v2/README.md and llm_context call it "Pyth Core", which matches Pyth's docs. Using one name everywhere would help (see inline comment).
  • Protocol name: both templates report protocol: Pyth. by_protocol("Pyth") now returns two templates, and the tests are updated for that. Only the template id tells the two receivers apart, which is fine, but it is worth knowing.

Comment thread crates/core/src/scenarios/README.md Outdated

**Currently supported protocols:**
- **Pyth v2** - Price oracle with 4 price feed templates (SOL/USD, BTC/USD, ETH/BTC, ETH/USD)
- **Pyth v2** - Price oracle with a price feed template for the default receiver (`pyth-price-feed-v2`) and one for the Pyth Core Compatible receiver (`pyth-core-compatible-price-feed-v2`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this line calls rec5EKMG… the "default receiver", but pyth/core-compatible/v2/README.md and the template's llm_context call it "Pyth Core", which is also Pyth's own name for it. Suggest Pyth Core receiver (pyth-price-feed-v2) and Pyth Core Compatible receiver (pyth-core-compatible-price-feed-v2) so readers don't take "default" to mean a third thing.

fresh as upstream's. A time written into the scenario is fixed when the scenario is created
and ages until it plays. To make a price stale on purpose, follow the TIME FIELDS steps of
pyth-price-feed-v2.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llm_context sends readers to "the TIME FIELDS steps of pyth-price-feed-v2", so the two templates' contexts are now coupled. If that section of the other template is renamed or reworded, this pointer silently goes stale. Consider adding a short version of the staleness recipe here, or a registry test that checks the referenced heading still exists in pyth-price-feed-v2's llm_context.

("price_message.price", 73, 8, 9_100_000_000_000),
("price_message.conf", 81, 8, 500_000_000),
("price_message.exponent", 89, 4, -10),
("price_message.publish_time", 93, 8, 1_800_000_000),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the comment says each value differs from the live one, but posted_slot = 500_000_000 is a slot mainnet will reach eventually (roughly 2027–2028 at the current rate). At that point the !diffs.is_empty() / per-field checks could pass or fail by coincidence. A clearly unreachable value such as u32::MAX as i64 * 4 would make the test permanent. The same reasoning loosely applies to publish_time = 1_800_000_000 (Jan 2027).

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description change is unrelated to the Pyth template. The PR body says it is shared with other open PRs, but if those land first this hunk will conflict or become a no-op. Consider moving it into its own small PR so the protocol PRs don't depend on each other.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds the pyth-core-compatible-price-feed-v2 template for PriceUpdateV2 accounts owned by the Pyth Core Compatible receiver (rec2HHDD…). The PR bundles that receiver's on-chain IDL (converted, with the account names capitalized so the discriminators match), registers it under the Pyth protocol, updates the registry counts and tests, and adds a feature-gated live test.

Assessment: looks good. I checked these points:

  • The registry adds every template IDL to registered_idls (register_builtin_template_idls), keyed by idl.address. So the new IDL is used for rec2… accounts and doesn't collide with the existing rec5… Pyth IDL, and the getAccountInfo + jsonParsed step in llm_context works.
  • The offsets in the live test are correct for a Full verification level: 8-byte discriminator + 32-byte write_authority, then the variant byte at 40 and feed_id at 41–72. Writing -10 as i64 and keeping the first 4 little-endian bytes gives the right i32 encoding.
  • The no-op round-trip check and the "only the written byte ranges changed" check are strong guards against IDL or layout drift.
  • The cross-reference to "TIME FIELDS" in pyth-price-feed-v2 points to a section that exists.

Notes:

  • Unrelated file in the diff: .github/workflows/claude-review.yml (141 lines) appears in base...head. It seems to come from merging feat/scenarios/override/pyth-pro-support, and the PR description doesn't mention it. Please confirm it's meant to ship with this PR, or rebase it out.
  • Test depends on a live account: the test pins one mainnet account (7AviUf9…) and assumes it is Full-verified. If that account is ever re-posted as Partial, the test fails at line 60 by design, but it then has to be updated by hand. Consider choosing the offsets from the variant byte instead of asserting it.
  • Shared protocol name: both collections declare protocol: Pyth, so by_protocol("Pyth") now returns two templates whose IDLs belong to different programs. This works today, but any consumer that assumes one IDL per protocol will mix them up. Using a separate protocol label, as kamino-scope does, would avoid that.

description: Uncertainty band around the price, in the same scale as price
- path: price_message.exponent
label: Exponent
description: Decimal exponent, normally -8 and -10 on some feeds. Rescales every price field

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "normally -8 and -10 on some feeds" reads awkwardly. Suggest: "Decimal exponent, usually -8 (-10 on some feeds)."

let live = fetch(CORE_COMPATIBLE_SOL_USD_FEED).await;
assert_eq!(live.owner.to_string(), PYTH_CORE_COMPATIBLE_RECEIVER);
// A Partial verification level shifts every offset below by one.
assert_eq!(live.data[40], 1, "verification level should be Full");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-fails if Pyth ever re-posts this account at a Partial level, and then the test needs a new address or new offsets. You could keep the assertion but derive a base offset from live.data[40] (0 → +1 byte for num_signatures), so the test survives that change. Optional.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Summary

Adds the pyth-core-compatible-price-feed-v2 template for PriceUpdateV2 accounts owned by rec2HHDD…. The PR bundles the receiver's converted IDL, with account names capitalized so the discriminators match (I checked that PriceUpdateV2 → [34,241,35,99,…]). The new template takes its address from the caller.

The PR also adds keep-alive republishing (publish_time/posted_slot) to both Pyth templates and rewrites the existing pyth-price-feed-v2 llm_context. The tests are well targeted: byte-offset diffs against a live account, plus a time-travel check.

Issues

  • Scope and behaviour change not described. The description says "No protocol Rust: YAML, IDL, the registry entry and a live test". In fact the PR also changes how the existing pyth-price-feed-v2 behaves. Any publish_time/posted_slot a user sets is now silently overwritten every slot, and the documented stale-price workflow (pause clock → set publish_time → time travel) is removed. Existing scenarios that override a feed to test staleness will stop working. The title and description should say so.
  • Missing clock-sensitive tag on the new collection. Details are in the inline comment.
  • Unrelated file. .github/workflows/claude-review.yml shows up in the diff. If it comes from a stale base, rebase; otherwise split it into its own PR.
  • Live-mainnet fragility (minor). The integration tests hard-code mainnet addresses and require verification_level == Full at byte 40. They're feature-gated, which is fine. A clearer failure message when the account turns Partial would help, rather than only noting it in a comment.
  • Ordering inside pyth-price-feed-v2's llm_context (nit). The new "a value set for publish_time … never takes effect" text sits below the template's list of properties, which still advertises publish_time and posted_slot. That template, and the new core-compatible one, still list both as settable properties in Studio. Consider dropping them from properties, or marking them as overwritten, so users don't set values that can never take effect.

crates/core/src/scenarios/protocols/pyth/core-compatible/v2/overrides.yaml:91

The tag list is missing clock-sensitive. pyth/v2/overrides.yaml has it, and the scenarios README says it marks collections with a time the program checks against the Clock, such as Pyth's publish_time. Studio uses it to warn before pausing the clock. Without it, pausing while this scenario plays gets no warning. The warning is still needed because a feed the scenario doesn't override still ages. Add the tag, and extend test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template to check the new template too.


crates/core/src/scenarios/protocols/pyth/v2/overrides.yaml:227

This changes the behaviour of the existing template: a publish_time set by the user is now overwritten every slot. The old way to test staleness (set publish_time, then time travel) no longer works for overridden feeds, so scenarios that relied on it will break. The PR description doesn't mention this. Also, publish_time and posted_slot are still listed as editable properties in this template (and in the new one), so Studio users can set values that can never take effect.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Summary

This PR adds pyth-core-compatible-price-feed-v2 for PriceUpdateV2 accounts owned by rec2HHDD…. It comes with the converted IDL, a README recording where the IDL came from, a keep-alive file, registry wiring, unit tests and a live mainnet test. It also changes the existing pyth-price-feed-v2: that template now imports an owned keep-alive (pyth/v2/keep-alive.yaml), and its stale-price guidance in llm_context is rewritten.

The structure is clean and the tests are strong. The live test checks byte offsets and that only the price fields change, and the keep-alive test covers an overridden feed, a feed loaded later, a feed with keepAlive: false, and a time travel.

Issues

  • Missing clock-sensitive tag. The new collection doesn't carry the tag that pyth/v2 has. Studio uses that tag to warn before pausing the clock, and these accounts are just as clock-sensitive. See the inline comment.
  • The PR does more than the title and body say. Turning on owned keep-alive for every Core receiver price account changes behaviour for existing pyth-price-feed-v2 users: after any Pyth override plays, every loaded feed's publish_time and posted_slot are rewritten each slot. The old "pause clock + time travel" guidance for staleness is also removed. Please say this in the PR description, or split it into its own PR.
  • The two llm_context texts have drifted apart. The Core Compatible STALE PRICE section leaves out the reminder that surfnet_timeTravel takes milliseconds while publish_time is in seconds; the Core template still has it. Copy it over so the two templates guide the agent the same way.
  • Live tests hard-code the byte layout. They assume Full verification (offset 40 = 1). That's acceptable, since the tests check it and fail with a clear message, but they will break whenever the mainnet account changes layout. That cost is already accepted by gating them behind integration-tests.

Overall this looks good once the tag is added and the change to the existing template is called out.


crates/core/src/scenarios/protocols/pyth/core-compatible/v2/overrides.yaml:105

This collection is missing the clock-sensitive tag that pyth/v2/overrides.yaml has. These accounts carry a publish_time and posted_slot that consumers check against the Clock, so Studio should warn before pausing the clock for them in the same way. Please add - clock-sensitive, and consider adding pyth-core-compatible-price-feed-v2 to test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template (the oracle tag count test is unaffected).


crates/core/src/scenarios/protocols/pyth/core-compatible/v2/overrides.yaml:175

The equivalent section in pyth-price-feed-v2 goes on to explain how to test a staleness check: use surfnet_timeTravel, whose absoluteTimestamp is in milliseconds while the Clock and publish_time are in seconds. That units note is missing here, and agents commonly trip on it. Please keep the two templates' guidance in sync.


crates/core/src/scenarios/protocols/pyth/v2/overrides.yaml:213

This changes behaviour for existing pyth-price-feed-v2 users. Once any override of this template plays, every loaded rec5EKMG… price account has publish_time and posted_slot rewritten every slot, including feeds the user never touched. The PR title and body describe only the new template, so please call this change out (or move it to its own PR) so reviewers and users know about it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview. This PR adds pyth-core-compatible-price-feed-v2. It writes PriceUpdateV2 accounts owned by rec2HHDD…, and the account to write is chosen by address (type: pubkey). It also bundles the receiver's IDL with the account names capitalized, plus a README that records the source. Pyth templates now use keep-alive: every price account of a receiver that the surfnet has loaded gets its publish_time and posted_slot rewritten each slot. The unit tests and the live integration tests are thorough. The byte-diff checks against a real mainnet account and the stale-feed and loaded-later cases are especially good.

Scope. The description says the PR is the new template, its IDL, the registry entry and a test. The diff also changes how the existing pyth-price-feed-v2 behaves:

  • It adds a keep_alive import over every account owned by rec5EKMG….
  • It rewrites that template's llm_context, dropping the old pause-clock / read-clock / time-travel recipe for stale prices.

From the moment a scenario plays, every loaded Pyth Core feed gets republished each slot, including feeds no override names. Users who relied on feeds aging naturally will see different results. This should be stated in the PR description, or split into its own PR.

Other notes.

  • The new IDL's account discriminator for PriceUpdateV2 ([34,241,35,99,157,126,244,205]) matches the one in the existing pyth/v2/idl.json, so the capitalization fix described in the README checks out.
  • The MCP params description change has nothing to do with Pyth. The body says it is shared with other PRs, so whichever PR merges first should own it, to avoid conflicts.

tags:
- oracle
- price-feed
- defi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clock-sensitive tag is missing here, though pyth/v2/overrides.yaml has it. These accounts carry the same publish_time and posted_slot checked against the Clock. Without the tag, Studio won't warn before pausing the clock, even though fetched accounts can end up ahead of the surfnet's time. Please add the tag, and add this template id to test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template.

description: Decimal exponent, normally -8 and -10 on some feeds. Rescales every price field
- path: price_message.publish_time
label: Publish Time
description: Unix seconds of this update, interpreted against the Surfnet clock

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish_time (here) and posted_slot (line 45) are still offered as editable properties. But the llm_context says a value set for them 'never takes effect' unless keepAlive: false. Studio users only see the property description, not the llm_context. Consider saying so in the description, e.g. "…; only takes effect with keepAlive: false". The same applies to pyth/v2/overrides.yaml.

- defi
- clock-sensitive

keep_alive:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turns on keep-alive for every loaded rec5EKMG… account whenever a pyth-price-feed-v2 override plays. That changes behavior for existing scenarios: feeds no override names no longer go stale. The PR description doesn't mention it. Please call it out, or move it to a separate PR, since this PR is described as only adding the Core Compatible template.


pub fn load_pyth_overrides(&mut self) {
self.load_protocol_overrides(PYTH_V2_IDL_CONTENT, PYTH_V2_OVERRIDES_CONTENT, "pyth");
let idl = serde_json::from_str(PYTH_V2_IDL_CONTENT).expect("pyth idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: load_protocol_overrides already parses this IDL. Parsing it again here with expect is harmless for an embedded constant. If other protocols repeat this pattern, a small helper (or having load_keep_alive accept the IDL string) would remove the duplication.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds pyth-core-compatible-price-feed-v2 for PriceUpdateV2 accounts owned by rec2HHDD…. It bundles the receiver's IDL with the two account names capitalized so the discriminators match live accounts, and adds keep-alive publication templates for both Pyth receivers. The YAML/IDL wiring looks right. The integration tests check byte offsets and that only the price fields change, which is good. The registry unit tests have been updated for the new counts.

Concerns

  • The PR description leaves out a behavior change to pyth-price-feed-v2. The description says "No protocol Rust", but the PR also adds pyth/v2/keep-alive.yaml and imports it into pyth-price-feed-v2. Every existing scenario that uses that template now republishes every loaded Pyth Core price account every slot. The old stale-price recipe (set publish_time, then time travel) is removed and would no longer work: whatever value it writes is overwritten. Please say so in the PR body and changelog, or split it into its own PR.
  • clock-sensitive tag missing. The new collection holds clock-checked accounts just like pyth/v2, but it isn't tagged, so Studio won't warn before pausing the clock. test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template should also cover the new template.
  • Inaccurate llm_context. The "never takes effect" wording ignores keepAlive: false, and the text cites Project 0's config.oracle_keys.0 although no Project 0 templates exist in this repo.
  • Minor: load_pyth_overrides parses each IDL JSON a second time just for load_keep_alive. It works, but it could reuse the IDL that load_protocol_overrides already parsed.

tags:
- oracle
- price-feed
- defi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the clock-sensitive tag that pyth/v2/overrides.yaml carries. These accounts hold publish_time/posted_slot, which consumers check against the Clock. Without the tag, Studio won't warn before pausing the clock. Please add the tag, and extend test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template to assert it for pyth-core-compatible-price-feed-v2.

Surfnet clock's unix seconds and posted_slot the slot, also after a time travel. This includes
feeds no override names and, from the next slot, feeds a transaction loads later, so a
liquidation that reads the borrower's other feeds finds them fresh. Price, conf and the EMA
fields keep their values. A value set for publish_time or posted_slot never takes effect, so

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"never takes effect" is only true while keep-alive is on. With keepAlive: false, a value set for publish_time/posted_slot is written and stays. Consider "never takes effect unless the override sets keepAlive: false", here and in pyth/v2/overrides.yaml:276. Otherwise an LLM may never set a deliberate stale timestamp.

- defi
- clock-sensitive

keep_alive:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes how the existing pyth-price-feed-v2 behaves: every loaded Pyth Core price account is now republished every slot once any override of this template plays. The previous stale-price recipe (set publish_time, then time travel) is removed and would no longer work. The PR description doesn't mention this. Please call it out (breaking for existing scenarios) or move it to a separate PR.

WHICH RECEIVER: this template writes price accounts of the Pyth Core receiver
rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ, which Save and Kamino read. Project 0 reads the
Pyth Core Compatible receiver instead: for a Project 0 price use
pyth-core-compatible-price-feed-v2 on the bank's config.oracle_keys.0. When a protocol's own

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This points to Project 0's config.oracle_keys.0, but no Project 0 templates are registered in this repo. Unless those templates land first, the instruction can't be followed as written. Consider wording it generically, or adding it only together with the Project 0 templates.


pub fn load_pyth_overrides(&mut self) {
self.load_protocol_overrides(PYTH_V2_IDL_CONTENT, PYTH_V2_OVERRIDES_CONTENT, "pyth");
let idl = serde_json::from_str(PYTH_V2_IDL_CONTENT).expect("pyth idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: load_protocol_overrides already parses this IDL JSON, and it's parsed again here just for load_keep_alive, and again on line 253 for the core compatible IDL. A small helper, or having load_protocol_overrides return the parsed IDL, would avoid the duplicate parsing and the duplicate expect strings.

@github-actions

Copy link
Copy Markdown

Summary

Adds pyth-core-compatible-price-feed-v2 for PriceUpdateV2 accounts owned by the Pyth Core Compatible receiver (rec2HH…). It includes a bundled IDL whose account names were capitalized before conversion so the discriminators match, keep-alive imports for both Pyth receivers, registry tests and a live mainnet integration test. The approach is sound. The IDL provenance is well documented in the README. The integration test is thorough: it checks byte offsets, confirms nothing else changes, and covers keep-alive across slots and time travel.

Issues

  • Missing clock-sensitive tag. The new collection has no clock-sensitive tag, but these accounts are just as clock-checked as the Pyth Core ones. test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template should also cover the new template.
  • Dangling cross-reference. pyth/v2/overrides.yaml points the LLM to "the bank's config.oracle_keys.0" for Project 0, but no Project 0 / marginfi template exists in this repo. The guidance only makes sense once that PR lands.
  • Duplicated guidance. The "price stays published" and "stale price" paragraphs are copied almost word for word between the two templates. That's acceptable for llm_context, but they will drift. Consider moving the shared wording into a common source, or at least adding a note to keep them in sync.

Minor: the integration test depends on a specific live mainnet account (7AviUf…) staying at Full verification. That is asserted explicitly, which is good, but it remains a potential source of flakiness in CI.


crates/core/src/scenarios/protocols/pyth/core-compatible/v2/overrides.yaml:9

Missing clock-sensitive. These accounts carry publish_time and posted_slot checked against the Clock, exactly like pyth/v2. Without the tag, Studio won't warn before pausing the clock for scenarios using this template. Please add it and extend test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template to cover pyth-core-compatible-price-feed-v2.


crates/core/src/scenarios/protocols/pyth/v2/overrides.yaml:240

config.oracle_keys.0 refers to a Project 0 bank template that doesn't exist in this repo yet. An LLM following this has no template to read the field from. Either make it generic ("the field the consumer stores the oracle in") or land it together with the Project 0 templates.


crates/core/src/scenarios/protocols/pyth/core-compatible/v2/overrides.yaml:153

This block and the STALE PRICE block below are near copies of the text in pyth/v2/overrides.yaml. A future keep-alive behaviour change has to be edited in both places. Consider a shared snippet, or at least a comment saying the two must stay in sync.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds a pyth-core-compatible-price-feed-v2 template for PriceUpdateV2 accounts owned by the Core Compatible receiver (rec2HH…). The PR bundles the receiver's converted IDL, a registry entry, unit tests and live mainnet tests. It also adds an owned-kind keep-alive (publish_time/posted_slot ← $unix_s/$slot) to both Pyth templates.

The new template looks right. The IDL provenance in the README is good, and the byte-offset live test is thorough.

Issues

  • Undisclosed behaviour change to pyth-price-feed-v2. The PR body says it adds a template. It also changes the existing one, and keepAlive defaults to true (default_keep_alive). Any existing scenario that uses pyth-price-feed-v2 will now republish every loaded Pyth Core price account every slot, and any publish_time/posted_slot it sets is silently overwritten. Scenarios built on the old stale-price recipe (set publish_time, then time-travel) stop producing stale prices unless they add keepAlive: false. This needs to be in the PR description and changelog, or split into its own PR.
  • Old guidance dropped. The pyth-price-feed-v2 llm_context no longer says that publish_time is read against the surfnet clock and not wall-clock time. That still matters when keepAlive: false is used to set a time by hand.
  • Unrelated change in the same PR. The MCP params schema description in crates/mcp/src/surfpool/mod.rs has nothing to do with this feature. The PR says it is shared with other PRs, which will cause merge conflicts or churn whichever lands first.

tags:
- oracle
- price-feed
- defi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clock-sensitive tag is missing. pyth/v2/overrides.yaml has it, and the scenarios README says it covers Pyth's publish_time. Without it, Studio won't warn when a scenario that uses this template is played with the clock paused, even though this template also writes publish_time and posted_slot. Add - clock-sensitive, and add the template to test_clock_sensitive_tag_marks_pyth_and_every_phoenix_template.

- defi
- clock-sensitive

keep_alive:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the behaviour of an existing template, and the PR description doesn't mention it. keepAlive defaults to true, so every existing pyth-price-feed-v2 scenario now rewrites publish_time/posted_slot on all loaded Pyth Core price accounts every slot. An explicit publish_time is silently ignored, and the old stale-price recipe no longer works. Please call this out, or split it into its own PR.

WHICH RECEIVER: this template writes price accounts of the Pyth Core receiver
rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ, which Save and Kamino read. Project 0 reads the
Pyth Core Compatible receiver instead: for a Project 0 price use
pyth-core-compatible-price-feed-v2 on the bank's config.oracle_keys.0. When a protocol's own

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This points to config.oracle_keys.0 and to Project 0 templates, which are not in this repo; the only Project 0 reference in the scenarios is this line. Until that PR lands, the LLM is told to follow a field no template defines. Either make the wording generic or make this PR depend on the Project 0 PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants