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 to 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
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.writeContract call per chain, issued in parallel via Promise.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:
Only the 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 the publisher constructor argument before you deploy the contracts:
The command prints the wallet address to stdout. Save it for the next step.
Step 5: Deploy ReserveAggregator on both chains
Deploy to Base Sepolia:
And Arbitrum Sepolia:
Record both deployed addresses.
--broadcast must come before --constructor-args — forge treats --constructor-args as variadic, so any flag that follows it gets consumed as another positional argument and the transaction is never sent.
Step 6: Wire in the addresses
Open src/tasks/nav-oracle.ts and replace the two address constants near the top with the ones you just deployed:
Step 7: Deploy to Goldsky
The task fires on the next 5-minute boundary. Watch it:
You should see a line like:
Verify on-chain by reading back latestRoundData():
The answer field is the total NAV scaled to 18 decimals; updatedAt is the custodian’s asOf timestamp.
Customization
Swap the data source
Change CUSTODIAN_URL to your own endpoint. Your API must return:
Amounts are human-readable USD — the task scales to 18 decimals before writing on-chain.
Add or swap chains
Add another wallet.writeContract(...) inside the Promise.allSettled block, targeting a different evm.chains.<name> and contract address:
Deploy a matching ReserveAggregator to that chain first.
Change the publish cadence
Real Proof-of-Reserves / NAV feeds typically publish hourly or daily. Change the cron expression in compose.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, call setPublisher(newAddress) from the current publisher. The old wallet loses updateNav permissions; only the new one can publish.
Resources