This guide walks you through building a Proof-of-Reserves / NAV oracle for a tokenized RWA fund. A single Compose task runs every 5 minutes, fetches a structured NAV bundle from a custodian endpoint, and publishes it toDocumentation Index
Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt
Use this file to discover all available pages before exploring further.
ReserveAggregator contracts on Base Sepolia and Arbitrum Sepolia in one run. The on-chain contract implements AggregatorV3Interface, so any existing Chainlink consumer can read it unchanged.
How it works
Each cycle:- Fetch the NAV bundle from a custodian JSON endpoint. If the response carries
ripcord: true, the task logs a skip message and returns without publishing. - Scale human USD numbers to 18-decimal fixed point, matching Chainlink’s convention for USD-denominated feeds.
- Publish to both chains independently via
Promise.allSettled. A failure on one chain does not block the other — the next cron cycle reconciles.
Prerequisites
- Goldsky CLI installed
- Foundry for contract deploys
- Testnet ETH on Base Sepolia and Arbitrum Sepolia to deploy the contracts
Project structure
Step 1: Set up the project
Clone the example repository:Step 2: Understand the task
nav-oracle.ts fetches the bundle, validates the ripcord, and publishes to both chains:
Key Compose features used
- Multi-chain writes from a single task — one
wallet.writeContractcall per chain, issued in parallel viaPromise.allSettled. sponsorGas: true— the publisher wallet is a Compose-managed Privy wallet whose transactions are gas-sponsored by Goldsky. No ETH needed on the publisher ever.context.fetch— retries the custodian call with exponential backoff before surfacing a failure.- Ripcord pattern — the data source itself carries a boolean kill-switch the operator can flip out-of-band. Useful when the upstream has a known issue and you want the oracle to pause without a redeploy.
Step 3: Understand the contract
ReserveAggregator.sol is a single-operator, AggregatorV3Interface-compatible publisher. The full bundle is stored in one struct; the scalar totalNav is exposed via the standard Chainlink reader so existing consumers work unchanged:
publisher address set in the constructor can call updateNav. That address is your Compose-managed wallet.
Step 4: Pre-create the publisher wallet
Compose’s wallet-create command provisions the named wallet in the cloud and prints its address, so you can pass that address as thepublisher constructor argument before you deploy the contracts:
Step 5: Deploy ReserveAggregator on both chains
Deploy to Base Sepolia:
Step 6: Wire in the addresses
Opensrc/tasks/nav-oracle.ts and replace the two address constants near the top with the ones you just deployed:
Step 7: Deploy to Goldsky
latestRoundData():
answer field is the total NAV scaled to 18 decimals; updatedAt is the custodian’s asOf timestamp.
Customization
Swap the data source
ChangeCUSTODIAN_URL to your own endpoint. Your API must return:
Add or swap chains
Add anotherwallet.writeContract(...) inside the Promise.allSettled block, targeting a different evm.chains.<name> and contract address:
ReserveAggregator to that chain first.
Change the publish cadence
Real Proof-of-Reserves / NAV feeds typically publish hourly or daily. Change the cron expression incompose.yaml:
Use the ripcord
Any host can flip the kill-switch by returning"ripcord": true from the custodian endpoint. The next task run logs Ripcord engaged … and skips the publish without erroring — Compose’s retry logic is not triggered. Flip it back to false and publishing resumes on the following cycle.
Rotate the publisher
If you need to re-create the Compose wallet or move to a different key, callsetPublisher(newAddress) from the current publisher. The old wallet loses updateNav permissions; only the new one can publish.