Pilot pack · Lending

Treasury in Aave, within limits you set, back when you need it.

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.

Demo

The console, in three minutes.

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.

How it works

One key, two contracts, a treasurer for the large amounts.

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.

See it in code ↓

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] --> AV
What is real, where

Nothing of value, on either network.

On 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]
    end
What the pack brings

Four parts, ready for a pilot.

The pack connects the Aave market and the Chainlink feeds that already run on the network; there is no contract of ours to trust.

Contracts

The real Aave and Chainlink

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 ↓

Policy

Your key, two contracts

An exact approval to the Pool, supplies only for the treasury, a daily cap and a treasurer for large amounts. See it in code ↓

Supply and withdraw

By name, in order

Approve and supply as one ordered batch; withdraw part or everything. See it in code ↓

Read

Position, rate, prices

What the treasury holds in Aave, what it earns, and prices with their age. See it in code ↓

The controls

What the treasurer can rely on.

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.

  • Declarative policies decide before anything happens: limits per operation, payer and period, held even when several workers decide at once.
  • Payout rules that read the payment itself: only listed beneficiaries, a daily cap in the token's own units, and approval above an amount.
  • Operations above a threshold wait for a quorum of approvers, who sign an EIP-712 digest that names neither nonce nor fee.
  • Every approval, signature and decision is recorded with who, when and what, and every change made through the API lands in an audit log.
  • Ordered batches from one identity, from the library: consecutive nonces, and the steps behind a failed one cancelled when you ask for it.
  • Submitting the same intent twice with the same idempotency key returns the same transaction: a retry never becomes a second payment.
  • Every failure comes with a report: the stage, a stable cause, whether a retry can work and on what condition, and the suggested action.
  • Read-only calls to any contract (a price, a balance, a position), decoded, from the library.
  • A ready-made treasury pilot pack for Aave V3: the Pool, the treasury's asset (USDC; on Sepolia EURS, while Aave's test USDC there is over its supply cap) and its aToken, Aave's price oracle and four Chainlink price feeds verified against the chain at a pinned block, a policy that holds the treasury's key to the Pool and that asset with an exact approval, supplies only for the treasury, a daily cap and approval above a threshold, and a one-command setup.
Evidence

Proven twice: against the real market on a copy of mainnet, and on Sepolia itself.

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 SepoliaOutcomeTransaction
10,000 test EURS from Aave's faucet, to the treasuryConfirmed0xb9891169…9f8962
Supply 100 EURS: the exact approvalConfirmed, step 1 of 20xb288185c…c490e6
Supply 100 EURS: the supplyConfirmed, step 2 of 2, once the approval was mined0x6a9d8a51…1850d8
Supply 600 EURS: the exact approvalConfirmed, step 1 of 20x4150a1d2…e79d4a
Supply 600 EURS: the supplyHeld until the treasurer approved; then confirmed0x84c87f7d…e7a427
An approval to another spenderRefused by the policy; nothing sentnone
Supply 400 EURS, past the daily cap of 1,000: the approvalConfirmed: ordered is not atomic0x0d3ba971…3e4603
Supply 400 EURS, past the daily cap of 1,000: the supplyRefused by the policy, limit exceeded; nothing sentnone
Withdraw 50 EURSConfirmed, without approval0x0e1798c2…ccee9f
Withdraw everything, interest includedConfirmed; the position reads zero0x983b1ac2…c79c5b

The treasury's address is 0x10253fc9…41fe91; its key was generated and is held by our demo sandbox.

For engineers

How it is done, in code.

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.

Step 1

Verify

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
Step 2

The policy

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 treasurers
Step 3

Supply

One 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
Step 4

Position

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 it
Step 5

Prices

Each 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 price
Step 6

Withdraw

Part 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 refused
Demo

See it run, then try it on yours.

We 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.