Setup: Compose nav-oracle
This skill makes the cloned nav-oracle example runnable under the user's own Goldsky account. On a cron (default every 5 minutes) the app fetches a NAV (net asset value) bundle from a custodian HTTP endpoint, scales the values to 18-decimal fixed-point, and writes them to ReserveAggregator contracts on two chains in parallel via a single Compose-managed wallet.
Assume the user has never used Goldsky Compose before. Do not skip preflight.
Non-negotiables
- Never run
forge create,goldsky compose deploy,git push, orgh repo createwithout showing the exact command first and getting explicit confirmation. - The
publisherconstructor arg onReserveAggregatormust be the Compose-managed wallet address. Anything else reverts every write withOnlyPublisher(). This wallet is created by Compose on first deploy — you cannot pick it; you must deploy Compose first to find out what it is. - The SALT-like detail here is the custodian's JSON schema. If the custodian response doesn't match the
CustodianResponseinterface insrc/tasks/nav-oracle.tsexactly (accountName,asOf,cash,tbills,repo,totalNav,ripcord), publishes silently drop or throw on parse. - Do not touch
src/lib/scaling.ts. The 18-decimal scaling is coupled to the contract'sdecimals()return value (also hardcoded at 18).
Variable handling for agents
When this skill says $FOO, capture the literal value from the prior command's output and substitute it directly into the next command. Do not rely on shell variables persisting between separate Bash tool invocations — each invocation gets a fresh shell with no env carryover from earlier commands.
Preflight
goldskyCLI — rungoldsky --version. If missing, install per https://docs.goldsky.com/reference/cli.goldskyauthenticated — rungoldsky project list. If it errors, stop and tell the user: "Please rungoldsky loginin your terminal — browser flow. Tell me to continue when you see the success message." Do not spawngoldsky loginfrom Bash — it needs an interactive browser. Alternative: the user passes--token <token>on each command.deno— rundeno --version. Install withcurl -fsSL https://deno.land/install.sh | shif missing.foundry— runforge --version. Contract deployment needs it. Install:curl -L https://foundry.paradigm.xyz | bash && foundryup.
Step 1 — Configuration interview
- "App name?" (default:
nav-oracle) — becomes the top-levelname:field incompose.yamland the deploy URL path. - "Which two chains?" (default:
baseSepolia+arbitrumSepolia) — this example is hardcoded to publish to two chains. If the user wants a single chain or different chains, note that in Step 3 you'll also need to edit the twoBASE_SEPOLIA_AGGREGATOR/ARBITRUM_SEPOLIA_AGGREGATORconstants and the correspondingwallet.writeContract(evm.chains.*, ...)calls in thePromise.allSettled([...])block ofsrc/tasks/nav-oracle.ts. - "Custodian endpoint URL?" — the HTTP endpoint that serves the NAV JSON. Default is the mock at
https://raw.githubusercontent.com/goldsky-io/documentation-examples/main/compose/nav-oracle/mock-custodian.json. If the user is just demoing, leave it; otherwise ask for their own URL. - "Feed description string for each chain?" — human-readable label stored on-chain (e.g.
"Example RWA Fund I NAV / USD"). - "How often should the cron run?" (default:
*/5 * * * *) — custodian-dependent. Hourly (0 * * * *) is common for real PoR feeds. - "Publish to a new GitHub repo?" — optional.
Step 2 — Provision the publisher wallet
The publisher wallet is named nav-oracle-publisher (matches the name: in the evm.wallet({ name: "nav-oracle-publisher", sponsorGas: true }) call inside src/tasks/nav-oracle.ts). Provision it and print its address without needing to deploy first:
goldsky compose wallet create nav-oracle-publisher
Save the printed address — call it $PUBLISHER.
Note: you do not need to fund this wallet. The task passes sponsorGas: true to evm.wallet({...}) in src/tasks/nav-oracle.ts, so Goldsky covers gas on both chains.
Step 3 — Deploy ReserveAggregator on both chains
Output these two commands. Don't execute — the user runs them with their own funded EOA on each chain (they only need gas for the deploy tx, not for runtime).
# Base Sepolia
forge create contracts/ReserveAggregator.sol:ReserveAggregator \
--rpc-url https://sepolia.base.org \
--private-key $PRIVATE_KEY \
--broadcast \
--constructor-args $PUBLISHER "<feed description>"
# Arbitrum Sepolia
forge create contracts/ReserveAggregator.sol:ReserveAggregator \
--rpc-url https://sepolia-rollup.arbitrum.io/rpc \
--private-key $PRIVATE_KEY \
--broadcast \
--constructor-args $PUBLISHER "<feed description>"
Each prints Deployed to: 0x.... Capture both — call them $BASE_AGG and $ARB_AGG.
Step 4 — Wire aggregator addresses into the task
Edit src/tasks/nav-oracle.ts — use grep anchors, line numbers will drift over time:
- Find
const BASE_SEPOLIA_AGGREGATORand replace its address with"$BASE_AGG" - Find
const ARBITRUM_SEPOLIA_AGGREGATORand replace its address with"$ARB_AGG"
If the user picked a custom custodian URL in Step 1:
- Replace the
CUSTODIAN_URLstring near the top of the file with theirs.
If the user picked different chains in Step 1, also edit:
- The two
wallet.writeContract(evm.chains.baseSepolia, ...)andwallet.writeContract(evm.chains.arbitrumSepolia, ...)calls inside thePromise.allSettled([...])block — swap chains and (optionally) rename the constants to match.
If the user wants a custom cron cadence, edit the expression: under the cron trigger in compose.yaml.
Step 5 — Optional: publish to a new GitHub repo
git init
git add .
git ls-files --cached | grep -iE '(keypair\.json|\.env|private[._-]?key|\.pem|id_rsa)' && \
{ echo "ABORT: secret-shaped file staged"; exit 1; }
git commit -m "Initial commit: Compose nav-oracle"
gh repo create <user's repo name> --<public|private> --source=. --push
Step 6 — Redeploy Compose
goldsky compose deploy
Step 7 — Smoke test
Tail logs and wait for the next cron fire (up to 5 min):
goldsky compose logs
Good output:
Published <accountName> NAV=$<totalNav> — base:ok, arb:ok
Verify on chain explorers — each aggregator contract should show a NavUpdated event:
https://sepolia.basescan.org/address/$BASE_AGG#eventshttps://sepolia.arbiscan.io/address/$ARB_AGG#events
Sanity-test the ripcord: if the user controls the custodian endpoint, flip "ripcord": true in the response JSON. Next cron fire should log Ripcord engaged … and skip cleanly (not an error).
Troubleshooting
- Edits to
compose.yamlor source files don't take effect after redeploy. The local.compose/bundle cache is stale. Runrm -rf .compose/and redeploy. OnlyPublisher()revert.$PUBLISHERyou wired into the forge constructor doesn't match the Compose wallet. Only the address currently holding the publisher role can rotate it (setPublisherreverts unlessmsg.sender == publisher). If the wrong address went in at constructor time, redeploy is the only recovery path — the deployer EOA has no privileged role on the contract.- Task keeps logging
skipped: unconfigured. One ofBASE_SEPOLIA_AGGREGATOR/ARBITRUM_SEPOLIA_AGGREGATORis still the zero address. Re-checksrc/tasks/nav-oracle.tslines 22–23. - Custodian fetch fails with JSON parse error. The custodian response does not match the
CustodianResponseshape. All seven fields are required;cash/tbills/repo/totalNavmust be JSON numbers (not strings),asOfmust be ISO 8601,ripcordmust be boolean. - Both
baseSepoliaandarbitrumSepoliawrites fail. The task throws and the retry config kicks in (2 attempts, 2s/4s backoff). If it persists, checkhttps://sepolia.basescan.org/address/$BASE_AGGfor recent state — the contract may be self-destructed, paused by a badsetPublisher, or on the wrong chain. - One chain's write succeeds, the other fails. This is tolerated by design (
Promise.allSettled). Next cron cycle reconciles. Only worry if failures are persistent on one side — likely an RPC or aggregator-specific issue.
What you should NOT do
- Keep
sponsorGas: trueon theevm.wallet({...})call. Managed (Privy) wallets default totruealready; the explicit setting is for clarity. Do not flip it tofalse— the publisher would then need native gas on both chains. - Do not change
toScaled18insrc/lib/scaling.tsor thedecimals()return on the contract. They're coupled. - Do not add historical round support to
ReserveAggregator.solas part of setup — the README notes this is a demo simplification. If the user asks for it, that's a separate change, not setup. - Do not add a
PRIVATE_KEYsecret. The publisher is a Compose-managed wallet with gas sponsorship; the user's EOA is only used once, forforge create.