DeFi vaults are everywhere. Morpho, Kamino, Pendle, Yearn — billions in TVL running automated yield strategies. They rebalance on schedule, deploy capital on threshold triggers, and compound returns 24/7.
But none of them can answer a simple question: is momentum working for or against this entry?
A human trader checks RSI before deploying capital. If daily RSI is 75 — overbought, momentum fading — they wait. If it's 28 — oversold, potential reversal — they enter. Your vault? It doesn't know. It executes on schedule regardless.
The Problem: Condition-Blind Vaults
Today, vault strategies trigger on:
- Time — weekly rebalance, regardless of market state
- Price thresholds — rebalance when price moves ±5%
- TVL ratios — deploy when utilization drops
None of these factor in momentum. You can be perfectly right about an asset's fundamental value and still deploy capital at exactly the wrong time — RSI at 75, right before a 40% drawdown.
The problem isn't the strategy logic. It's that RSI doesn't exist on-chain. No smart contract can read it. Computing a 14-period RSI requires historical price data, resampling, and the Wilder smoothing formula — none of which is feasible in Solidity.
The Solution: An On-Chain RSI Oracle
What if your vault could read RSI directly from a Chainlink oracle — the same interface you already use for price feeds?
That's what this tutorial builds: a vault with an RSI circuit breaker. An oracle delivers pre-calculated RSI on-chain, and your contract checks it before allowing deposits. No off-chain server, no API keys, no centralized dependency.
An RSI gate doesn't predict the future. It says: don't deploy capital when momentum is strongly against you.
The Architecture (Important: This is Async)
On-chain RSI through Chainlink's Direct Request pattern is asynchronous — not like price feeds (latestRoundData()). You request a value, the oracle computes it off-chain, and delivers it to a callback.
This means the vault works in two phases:
-
Refresh phase: A keeper (or Chainlink Automation) calls
refreshRSI()periodically -
Deposit phase: Users call
deposit()— the contract checks the cached RSI value and its age
Lifecycle:
Keeper → refreshRSI() → sends LINK → Chainlink request
↓
Oracle computes RSI from 4 data sources
↓
Chainlink → fulfill() → stores lastRSI + timestamp
↓
User → deposit() → checks lastRSI < threshold && data < 2h old → allows
The Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
contract RSIGatedVault is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
// Pythia oracle config (Polygon mainnet)
address private constant ORACLE = 0xAA37710aF244514691629Aa15f4A5c271EaE6891;
address private constant LINK = 0xb0897686c545045aFc77CF20eC7A532E3120E0F1;
bytes32 private jobId;
uint256 private constant FEE = (1 * LINK_DIVISIBILITY) / 100; // 0.01 LINK
// RSI state
uint256 public lastRSI; // scaled 1e18 (e.g. 25.43 = 25430000000000000000)
uint256 public lastUpdated; // timestamp of last oracle fulfillment
uint256 public constant FRESHNESS = 2 hours;
uint256 public constant RSI_MAX = 35 * 1e18; // gate: deposit only when RSI < 35
// Vault accounting
mapping(address => uint256) public balances;
uint256 public totalDeposited;
string public feedName; // e.g. "uniswap_RSI_1D_14"
event RSIUpdated(uint256 rsi, uint256 timestamp);
event Deposited(address indexed user, uint256 amount, uint256 rsiAtDeposit);
constructor(bytes32 _jobId, string memory _feedName)
ConfirmedOwner(msg.sender)
{
_setChainlinkToken(LINK);
_setChainlinkOracle(ORACLE);
jobId = _jobId;
feedName = _feedName;
}
/// @notice Called by keeper or Chainlink Automation to refresh RSI
function refreshRSI() external returns (bytes32) {
Chainlink.Request memory req = _buildChainlinkRequest(
jobId,
address(this),
this.fulfill.selector
);
req._add("feed", feedName);
return _sendChainlinkRequest(req, FEE);
}
/// @notice Chainlink callback — stores updated RSI
function fulfill(bytes32 _requestId, uint256 _value)
public
recordChainlinkFulfillment(_requestId)
{
lastRSI = _value;
lastUpdated = block.timestamp;
emit RSIUpdated(_value, block.timestamp);
}
/// @notice Deposit POL — only allowed when RSI is below threshold
function deposit() external payable {
require(msg.value > 0, "Zero deposit");
require(
block.timestamp - lastUpdated <= FRESHNESS,
"RSI data stale — keeper must refresh"
);
require(
lastRSI < RSI_MAX,
"RSI too high — waiting for oversold conditions"
);
balances[msg.sender] += msg.value;
totalDeposited += msg.value;
emit Deposited(msg.sender, msg.value, lastRSI);
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
totalDeposited -= amount;
payable(msg.sender).transfer(amount);
}
}
Deploying and Testing
Step 1: Get LINK on Polygon mainnet
You need 0.01 LINK per RSI refresh. Get LINK at bridge.chain.link or via any Polygon DEX.
Step 2: Get the Job ID
Use the Pythia MCP server to discover available feeds and the job ID:
pip install pythia-oracle-mcp
# Then in Claude/Cursor/Windsurf:
# "What is the job ID for uniswap_RSI_1D_14?"
Step 3: Deploy
Deploy RSIGatedVault with:
-
_jobId: The Pythia job ID (bytes32, from MCP server) -
_feedName:"uniswap_RSI_1D_14"(or any available Pythia feed — EMA, RSI, VWAP, Bollinger, volatility, liquidity across multiple tokens and timeframes)
Step 4: Fund with LINK and call refreshRSI
// Transfer 0.1 LINK to the vault address, then:
vault.refreshRSI();
// Wait ~30 seconds for oracle fulfillment
// vault.lastRSI() now returns current RSI scaled by 1e18
Step 5: Automate with Chainlink Automation
Register the vault in Chainlink Automation to call refreshRSI() hourly. This keeps RSI data fresh without a centralized keeper.
Test Free with the Faucet
Before committing LINK, test with the Pythia Faucet on Polygon mainnet — pre-funded, no cost to you:
Faucet: 0x640fC3B9B607E324D7A3d89Fcb62C77Cc0Bd420A
The faucet contract holds its own LINK balance. Each request costs 0.01 LINK from the faucet's balance — you don't need to hold any LINK yourself. Up to 5 requests per day. All feeds available. If the faucet runs dry, anyone can top it up by sending LINK (ERC-677) to the faucet address.
What the Data Looks Like
Pythia delivers RSI values scaled by 1e18 on-chain. Here's how to interpret them:
| RSI Range | Meaning | Vault Behavior (threshold=35) |
|---|---|---|
| 0–30 | Oversold — momentum suggests potential reversal | Deposits allowed |
| 30–35 | Approaching neutral from oversold | Deposits allowed |
| 35–70 | Neutral — no strong signal | Deposits blocked |
| 70–100 | Overbought — momentum suggests potential pullback | Deposits blocked |
Example feeds available: uniswap_RSI_1D_14, aave_RSI_1D_14, bitcoin_RSI_1H_14, solana_RSI_1W_14 — any token Pythia serves, across 6 timeframes (5M, 15M, 1H, 4H, 1D, 1W).
When multiple DeFi tokens show oversold RSI simultaneously, the gate opens — precisely when technical analysis suggests momentum conditions favor entry.
Going Further
This is the simplest RSI gate. Production implementations might add:
Multi-indicator confirmation:
// Deposit only when RSI is oversold AND price is above EMA-20 (trend intact)
require(lastRSI < 35 * 1e18, "RSI too high");
require(lastPrice > lastEMA20, "Below trend — skip entry");
Bollinger Band entries:
Request uniswap_BOLLINGER_1D_LOWER — deposit only when price is within 2% of the lower band (double confirmation with RSI).
Bundle requests — get all indicators at once:
Use PythiaBundleConsumer to fetch EMA_20, EMA_50, RSI_14, BOLLINGER_UPPER, BOLLINGER_LOWER, VWAP, VOLATILITY, and LIQUIDITY in a single request:
- Analysis tier (0.03 LINK) — 1H/1D/1W timeframes
- Speed tier (0.05 LINK) — 5M timeframe
- Complete tier (0.10 LINK) — all 6 timeframes (5M, 15M, 1H, 4H, 1D, 1W)
Full indicator list: EMA_20, EMA_50, RSI_14, BOLLINGER_UPPER, BOLLINGER_LOWER, VWAP, VOLATILITY, LIQUIDITY across 6 timeframes for tokens including BTC, SOL, AAVE, UNI, LDO, Morpho, and more — new tokens added on demand.
Contract Addresses (Polygon Mainnet)
| Contract | Address | Fee |
|---|---|---|
| Faucet (free trial) | 0x640fC3B9B607E324D7A3d89Fcb62C77Cc0Bd420A |
Pre-funded |
| Discovery | 0xeC2865d66ae6Af47926B02edd942A756b394F820 |
0.01 LINK |
| Analysis (1H/1D/1W bundle) | 0x3b3aC62d73E537E3EF84D97aB5B84B51aF8dB316 |
0.03 LINK |
| Speed (5M bundle) | 0xC406e7d9AC385e7AB43cBD56C74ad487f085d47B |
0.05 LINK |
| Complete (all timeframes) | 0x2dEC98fd7173802b351d1E28d0Cd5DdD20C24252 |
0.10 LINK |
| LINK token (ERC-677) | 0xb0897686c545045aFc77CF20eC7A532E3120E0F1 |
— |
| Operator | 0xAA37710aF244514691629Aa15f4A5c271EaE6891 |
— |
Resources
- Live feeds and contracts: pythia.c3x-solutions.com
- MCP server (explore feeds in Claude/Cursor/Windsurf):
pip install pythia-oracle-mcp - LangChain integration:
pip install langchain-pythia
Pythia delivers pre-calculated technical indicators (EMA, RSI, VWAP, Bollinger Bands, volatility, liquidity) on-chain via standard Chainlink interface. Any token, any Chainlink-supported chain. pythia.c3x-solutions.com
Top comments (0)