Smart contracts are good at executing logic. They're terrible at knowing when to execute it.
If your contract needs to rebalance when RSI drops below 30, or pause deposits when volatility spikes above 8%, you have two options today: run an off-chain bot that polls an oracle every few minutes and calls your contract when conditions change, or set up a keeper network. Both require infrastructure you have to build, fund, and monitor.
There's a third option that works the way publish-subscribe patterns work everywhere else in software: subscribe to a condition, get notified when it's true, done.
This tutorial shows how to build a smart contract that subscribes to on-chain indicator alerts using a pub-sub oracle — no bots, no keepers, no off-chain infrastructure. One transaction to subscribe. One event to listen for.
The Interface
The Pythia Event Registry exposes a minimal interface for subscribing to calculated indicator alerts (RSI, EMA, Bollinger Bands, VWAP, volatility) delivered through a Chainlink oracle node:
interface IPythiaEventRegistry {
/// Emitted when a subscription's condition is met
event PythiaEvent(uint256 indexed eventId, int256 value);
/// Subscribe to an indicator alert. Approve LINK spending first.
function subscribe(
string calldata feedName, // e.g. "bitcoin_RSI_1D_14"
uint16 numDays, // 1-365 days to monitor
uint8 condition, // 0=ABOVE, 1=BELOW
int256 threshold // 8 decimals (RSI 30 = 3000000000)
) external returns (uint256 eventId);
/// Cancel subscription. Remaining whole days refunded in LINK.
function cancelSubscription(uint256 eventId) external;
/// Check if a subscription is still active
function isActive(uint256 eventId) external view returns (bool);
/// Get the LINK cost for N days at current price
function getCost(uint16 numDays) external view returns (uint256);
}
You call subscribe() with a feed name, how long to monitor, the condition (above or below a threshold), and a threshold value. The oracle monitors the indicator every 5 minutes. When the condition is met, it fires PythiaEvent on-chain with the triggering value and refunds your unused days in LINK.
No API keys. No webhook endpoints. No keeper contracts. Everything happens on-chain through standard Solidity events.
The Subscriber Contract
Here's a complete contract that wraps the registry interface. This is production code from the Pythia examples repository:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import "./interfaces/IPythiaEventRegistry.sol";
contract EventSubscriber is ConfirmedOwner {
LinkTokenInterface public immutable LINK;
IPythiaEventRegistry public registry;
uint256 public lastEventId;
event Subscribed(uint256 indexed eventId, string feed, uint8 condition, int256 threshold);
event Cancelled(uint256 indexed eventId);
constructor(address _link, address _registry) ConfirmedOwner(msg.sender) {
LINK = LinkTokenInterface(_link);
registry = IPythiaEventRegistry(_registry);
}
/// @notice Subscribe to an indicator alert. Fund this contract with LINK first.
/// @param feedName e.g. "pol_RSI_5M_14", "bitcoin_EMA_1H_20"
/// @param numDays 1-365 — how long to monitor
/// @param condition 0=ABOVE, 1=BELOW
/// @param threshold 8 decimals (e.g. RSI 30 = 3000000000)
/// @return eventId Listen for PythiaEvent(eventId) on the registry
function subscribe(
string calldata feedName,
uint16 numDays,
uint8 condition,
int256 threshold
) external onlyOwner returns (uint256 eventId) {
uint256 cost = registry.getCost(numDays);
LINK.approve(address(registry), cost);
eventId = registry.subscribe(feedName, numDays, condition, threshold);
lastEventId = eventId;
emit Subscribed(eventId, feedName, condition, threshold);
}
/// @notice Cancel a subscription. Remaining whole days refunded in LINK.
function cancel(uint256 eventId) external onlyOwner {
registry.cancelSubscription(eventId);
emit Cancelled(eventId);
}
/// @notice Check if a subscription is still active
function isActive(uint256 eventId) external view returns (bool) {
return registry.isActive(eventId);
}
/// @notice Update registry address (e.g. after upgrade)
function setRegistry(address _registry) external onlyOwner {
registry = IPythiaEventRegistry(_registry);
}
/// @notice Withdraw LINK from this contract
function withdrawLink() external onlyOwner {
LINK.transfer(msg.sender, LINK.balanceOf(address(this)));
}
}
The flow: fund the contract with LINK, call subscribe() with your condition, store the eventId, and listen for PythiaEvent on the registry. When the oracle detects your condition is met, the event fires on-chain. Your bot (or another contract) catches it and reacts.
Threshold Scaling
Thresholds use 8 decimal places — not 18 like standard ERC-20 values. This is the most common mistake when setting up subscriptions:
| Indicator | Target | Threshold Value |
|---|---|---|
| RSI below 30 | 30.0 | 3000000000 |
| RSI above 70 | 70.0 | 7000000000 |
| EMA below $2,500 | 2500.0 | 250000000000 |
| Volatility above 5% | 0.05 | 500000000 |
Two condition types are live:
- 0 = ABOVE — fires when the indicator value exceeds your threshold
- 1 = BELOW — fires when the indicator value drops below your threshold
Directional crossing conditions (CROSSES_ABOVE, CROSSES_BELOW) are accepted by the contract for forward compatibility but are not yet processed by the matcher.
Deploying to Testnet
The examples repo includes a Hardhat deploy script for both testnet and mainnet:
git clone https://github.com/pythia-the-oracle/pythia-oracle-examples
cd pythia-oracle-examples
npm install
# Deploy to Polygon Amoy testnet
npx hardhat run scripts/deploy-events.js --network amoy
The script wires the correct LINK token and registry addresses automatically. After deployment:
- Get test LINK from faucets.chain.link/polygon-amoy
-
Fund the contract — transfer LINK to your deployed
EventSubscriberaddress -
Subscribe — call
subscribe("bitcoin_RSI_1D_14", 3, 1, 3000000000)to monitor BTC RSI below 30 for 3 days -
Note the eventId from the
Subscribedevent -
Listen for
PythiaEvent(eventId)on the registry contract
Available feeds span multiple tokens (BTC, SOL, AAVE, UNI, MORPHO, CRV, COMP, and more) and indicators (EMA, RSI, Bollinger Bands, VWAP, volatility, liquidity) across timeframes from 5-minute to weekly.
What Happens When It Fires
The oracle monitors all active subscriptions every minute. When your condition matches:
- The matcher marks the subscription inactive
- A batch transaction calls
fireEvents()on the registry — one tx for multiple events -
PythiaEvent(eventId, value)is emitted with the triggering indicator value - Remaining whole days are refunded in LINK to your contract
Your subscriber listens for the event:
const myEventId = 42n;
// Real-time listener
registry.on("PythiaEvent", (eventId, value) => {
if (eventId === myEventId) {
const rsi = Number(value) / 1e8;
console.log(`RSI dropped to ${rsi} — condition met`);
// Trigger rebalance, close position, notify team
}
});
// Or backfill if your listener was offline — events are permanent
const filter = registry.filters.PythiaEvent(myEventId);
const events = await registry.queryFilter(filter, fromBlock, "latest");
Gas cost per fired event on Polygon: approximately $0.0001. Batch firing 50 events in a single transaction costs under $0.01.
Why Pub-Sub Beats Polling
If your contract polls an oracle for data on a fixed schedule, you pay per request whether the condition is met or not. With pub-sub, you pay a flat daily rate and the oracle handles the monitoring:
| Approach | Daily Cost | Infrastructure |
|---|---|---|
| Events (pub-sub) | 1 LINK/day | None — oracle monitors |
| Polling via Discovery | 2.88 LINK/day (288 req) | Keeper or bot |
| Polling via Complete | 28.8 LINK/day (288 req) | Keeper or bot |
Events cost 65% less than polling the cheapest tier, and 97% less than polling the complete tier. If the condition fires early, you get unused days refunded. If you change your mind, cancel and get the remaining days back.
The pub-sub pattern also eliminates infrastructure: no server to run, no keeper to fund, no cron job to monitor. Your contract subscribes once and waits for the callback.
AI Agent Integration
If you're building autonomous DeFi agents, indicator subscriptions can be discovered and configured through AI tooling:
MCP Server (works in Claude, Cursor, Windsurf):
pip install pythia-oracle-mcp
10 tools — discover available tokens, check feed health, look up contract addresses, get pricing, generate integration code.
LangChain tools (for agent frameworks):
pip install langchain-pythia
7 tools — PythiaListTokensTool, PythiaTokenFeedsTool, PythiaHealthCheckTool, PythiaContractsTool, PythiaPricingTool, and Events tools for subscription info and integration guides.
An AI agent can discover which indicators are available for a token, check current pricing, and generate the subscription parameters — all programmatically. The subscription itself is a single on-chain transaction that the agent (or human) approves.
Contract Addresses
| Network | Contract | Address |
|---|---|---|
| Polygon Mainnet | Event Registry | 0x73686087d737833C5223948a027E13B608623e21 |
| Polygon Mainnet | LINK Token (ERC-677) | 0xb0897686c545045aFc77CF20eC7A532E3120E0F1 |
| Amoy Testnet | Event Registry | 0x931Aa640d29E6C9D9fB3002749a52EC7fb277f9c |
| Amoy Testnet | LINK Token | 0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904 |
Resources
- Pythia Oracle — live feeds, contracts, documentation
- Events — how subscriptions work
- Example contracts and tests on GitHub
- MCP server:
pip install pythia-oracle-mcp - LangChain tools:
pip install langchain-pythia - Community: Telegram | X/Twitter
Pythia delivers pre-calculated technical indicators on-chain via standard Chainlink oracle interface — EMA, RSI, VWAP, Bollinger Bands, volatility, liquidity. Any token, any Chainlink-supported chain. Subscribe to conditions with Events, or request data directly. pythia.c3x-solutions.com
Top comments (0)