Your treasury supplies USDC to Aave, the largest lending market, and withdraws it whenever it needs to. A policy holds the treasury's key to Aave and USDC alone, caps what goes in each day and asks a treasurer for large amounts. The position, the rate it earns and market prices, with how old each one is, come back through the same API. Nothing shown here is real money.
Recorded from the real console on a sandbox over a public test network where nothing has value, with Aave's test USDC and prices Aave fixes there: a supply, a large one approved, the position and the prices, a withdrawal.
Your treasury system asks CryptoRail to supply or withdraw. The treasury policy reads each call itself: the treasury's key may only approve USDC to Aave's Pool, supply it for the treasury and withdraw it to the treasury. A supply is one ordered batch: an approval of exactly the amount, then the supply. A day's supplies have a cap, and a large one waits for a treasurer. Withdrawals need no approval: they only bring the treasury's money back.
flowchart TD
T[Your treasury system] --> P{Treasury policy}
P -->|another contract, spender or beneficiary| X[Refused with the reason, nothing sent]
P -->|past the daily cap| X
P -->|from the threshold| A[A treasurer approves]
P -->|within the rules| B[Ordered batch: exact approve, then supply]
A --> B
B --> AV[Aave V3 Pool: aUSDC for the treasury]
AV --> R[Position and rate, read through the API]
CL[Chainlink feeds] --> PR[Prices with their age]
W[Withdraw to the treasury] --> AVOn the mainnet fork, where every change is tested, the contracts are Aave's and Chainlink's real ones and the market is the real one, frozen at block 26,150,000 (8 October 2026): its rate, 4.72 % a year as Aave stores it, and its prices, ETH at 2,470.57 dollars. The treasury holds USDC there, written into that private copy; nothing reaches the real network.
On Sepolia, the public test network where the demo runs, the treasury holds EURS, a euro stablecoin, as Aave's test token, which Aave's faucet gives anyone. Why not USDC: on Aave's Sepolia market the test stablecoins (USDC, USDT, DAI) already hold more than their supply cap, so Aave refuses every new supply of them; EURS has no cap. The pack keeps USDC ready for when its cap has room. Aave fixes its prices there (1.126 dollars for EURS, 1.00 for USDC, not from Chainlink), and its rates are whatever strangers' test activity makes them. The Chainlink prices the pack reads on Sepolia, the euro in dollars among them, are Chainlink's live test feeds.
On mainnet, Aave prices USDC through a capped adapter over a Chainlink feed of its own, not the public feed the pack reads; the pack shows Aave's own price beside them, so the difference is visible.
flowchart LR
subgraph F[Mainnet fork, in every test]
F1[Real Aave market and Chainlink feeds] --> F2[Frozen at block 26,150,000]
F3[USDC written into the copy] --> F2
end
subgraph S[Sepolia, public test network]
S1[Aave's test market and test EURS] --> S2[Prices fixed by Aave]
S3[Chainlink's live test feeds] --> S4[Read with their age]
endThe pack connects the Aave market and the Chainlink feeds that already run on the network; there is no contract of ours to trust.
The Pool, the treasury's asset, its aToken, Aave's oracle and four price feeds, checked against the chain before use. See it in code ↓
An exact approval to the Pool, supplies only for the treasury, a daily cap and a treasurer for large amounts. See it in code ↓
Approve and supply as one ordered batch; withdraw part or everything. See it in code ↓
What the treasury holds in Aave, what it earns, and prices with their age. See it in code ↓
A retry never supplies twice, a refusal always says why, the steps of a supply run in the order given, and every large supply is recorded with who approved what and when.
On every change, an end-to-end scenario runs the pack against the real Aave V3 market and Chainlink feeds on a fork of Ethereum mainnet at a pinned block: two supplies, one approved by the treasurer, the position, rate and prices read, three refusals, a withdrawal, a year of interest and a withdrawal of everything. The scenario and the requirements behind it are listed on the evidence page. On 2026-10-12 the same pack ran on Sepolia from our demo console, in test EURS because Aave's test USDC there is over its supply cap; anyone can check each transaction on the explorer.
| On Sepolia | Outcome | Transaction |
|---|---|---|
| 10,000 test EURS from Aave's faucet, to the treasury | Confirmed | 0xb9891169…9f8962 |
| Supply 100 EURS: the exact approval | Confirmed, step 1 of 2 | 0xb288185c…c490e6 |
| Supply 100 EURS: the supply | Confirmed, step 2 of 2, once the approval was mined | 0x6a9d8a51…1850d8 |
| Supply 600 EURS: the exact approval | Confirmed, step 1 of 2 | 0x4150a1d2…e79d4a |
| Supply 600 EURS: the supply | Held until the treasurer approved; then confirmed | 0x84c87f7d…e7a427 |
| An approval to another spender | Refused by the policy; nothing sent | none |
| Supply 400 EURS, past the daily cap of 1,000: the approval | Confirmed: ordered is not atomic | 0x0d3ba971…3e4603 |
| Supply 400 EURS, past the daily cap of 1,000: the supply | Refused by the policy, limit exceeded; nothing sent | none |
| Withdraw 50 EURS | Confirmed, without approval | 0x0e1798c2…ccee9f |
| Withdraw everything, interest included | Confirmed; the position reads zero | 0x983b1ac2…c79c5b |
The treasury's address is 0x10253fc9…41fe91; its key was generated and is held by our demo sandbox.
The pack is a small library over CryptoRail's API: a manifest of Aave and Chainlink verified against the chain, the treasury policy for the service's rules engine, a setup that registers everything by name, and the calls and reads a treasury system makes. Your systems use the typed SDK, or plain HTTP. The calls below are the ones the scenario makes on every change.
Every address, code hash, implementation and selector was read from the chain at a pinned block. Verifying again before a pilot catches an upgraded Pool or a feed that is not the pair it claims; the setup then registers the contracts by name.
# Check Aave and Chainlink against the chain, at the pinned block: # Pool and aToken proxies, implementations and selectors, each asset # (USDC; on Sepolia also EURS), where Aave's oracle takes its price from, # the four feeds (EUR/USD included) node packages/pack-aave/dist/bin.js verify --network mainnet-fork node packages/pack-aave/dist/bin.js verify --network sepolia # Register the Pool, the asset (EURS on Sepolia), its aToken, Aave's oracle # and the feeds by name; --asset usdc when USDC's supply cap has room CRYPTORAIL_API_KEY=<admin key> \ node packages/pack-aave/dist/bin.js setup --network sepolia --url <service URL> # Run again: the same ids, nothing registered twice
The rules read inside each call: the spender of an approval, the beneficiary of a supply, the recipient of a withdrawal, the amount. The first rule holds the treasury's key to the two contracts and three functions and refuses anything else it asks for.
import { MAINNET_FORK, aavePolicy } from '@cryptorail/pack-aave'
const policy = aavePolicy({
version: 'treasury-2026-10', // recorded with every decision
network: MAINNET_FORK,
treasury: treasuryAddress, // the treasury's own key
dailySupplyCap: '100000', // USDC supplied per UTC day
approvalThreshold: '50000', // from here, a supply waits
approverIds: [treasurerId],
})
// What it says, as the rules engine reads it:
// deny the treasury's key, unless approve on USDC
// or supply / withdraw on the Pool
// deny approve when spender is not the Pool
// deny approve when amount is above the daily cap
// deny supply when onBehalfOf is not the treasury
// deny withdraw when to is not the treasury
// limit supply amount 100,000 USDC per day
// approve supply from 50,000 USDC, 1 of the treasurersOne ordered batch from the treasury's own identity: an approval of exactly the amount, then the supply. If the approval is refused, the supply behind it is cancelled.
import { createClient } from '@cryptorail/sdk'
import { supplyBatch } from '@cryptorail/pack-aave'
const cryptorail = createClient({ baseUrl, apiKey })
const actor = { network: MAINNET_FORK, contracts, // ids from the setup
identityId: treasuryId, treasury: treasuryAddress }
const { data } = await cryptorail.POST('/v1/transactions/batch', {
params: { header: { 'idempotency-key': `supply-${reference}` } },
body: supplyBatch(actor, 25_000_000_000n), // 25,000 USDC
})
// One ordered batch, one identity, consecutive nonces:
// 0 approve(spender: Pool, amount: 25000000000) on USDC
// 1 supply(asset: USDC, amount: 25000000000,
// onBehalfOf: treasury, referralCode: 0) on the Pool
// From 50,000 USDC, step 1 waits in PENDING_APPROVAL for a treasurer// supply(…, onBehalfOf: someone else): refused before anything is signed { "state": "REJECTED_BY_POLICY", "failure": { "stage": "policy", "cause": "policy.operation-denied", … } } // A supply past the daily cap: "cause": "policy.limit-exceeded" // Ordered is not atomic: the exact approve before it stays mined, and // only the treasury's own next supply can use it
The aToken balance is the position, interest included; the reserve's rate is what it earns now and changes with the market.
import { readPosition } from '@cryptorail/pack-aave'
// balanceOf on aEthUSDC and getReserveData on the Pool, through CryptoRail:
// reads, nothing signed
const position = await readPosition(cryptorail, MAINNET_FORK, contracts, treasuryAddress)
// position.suppliedUsdc about '25000' what could be withdrawn now, interest included
// position.walletUsdc '175000'
// position.rate { aprPercent: 4.72, apyPercent: 4.83 }
// currentLiquidityRate, a yearly rate in ray: the APR as Aave stores it,
// and the APY compounded every second, as Aave's own interface shows itEach Chainlink feed with its age, judged against the feed's heartbeat, and the price Aave itself uses. The figures shown are the ones frozen at the fork's block.
import { readPrices } from '@cryptorail/pack-aave'
// latestRoundData and decimals on each Chainlink feed, getAssetPrice on Aave's oracle
const prices = await readPrices(cryptorail, MAINNET_FORK, contracts, nowSeconds)
// prices.feeds: [
// { pair: 'ETH/USD', price: '2470.57', ageSeconds: 60n, heartbeatSeconds: 3600, stale: false },
// { pair: 'BTC/USD', price: '81628.13', ageSeconds: 2700n, heartbeatSeconds: 3600, stale: false },
// { pair: 'USDC/USD', price: '0.99986', ageSeconds: 16560n, heartbeatSeconds: 86400, stale: false },
// ]
// prices.aaveUsdc: { price: '0.99981945' } the price Aave itself uses
// stale: older than the feed's heartbeat. Informative: no rule depends on a pricePart of the position, or all of it with the interest earned, always to the treasury.
Possible next steps, not part of the pack: borrowing against the position, other assets and markets, and a rule that reads a price.
import { withdrawIntent } from '@cryptorail/pack-aave'
// Part of it
await cryptorail.POST('/v1/transactions', {
params: { header: { 'idempotency-key': `withdraw-${reference}` } },
body: withdrawIntent(actor, 10_000_000_000n), // 10,000 USDC
})
// Everything, interest included: amount type(uint256).max
await cryptorail.POST('/v1/transactions', {
params: { header: { 'idempotency-key': `withdraw-all-${reference}` } },
body: withdrawIntent(actor, 'all'),
})
// withdraw(asset: USDC, amount, to: treasury): no approval, it only brings
// the treasury's own money back to it; any other 'to' is refusedWe show the pack on our sandbox, on a public test network where nothing has value. A pilot gives you your own:A pilot environment stood up in minutes with nothing installed on your side.