Developing a subscription system for on-chain events via Push
Push Protocol (formerly EPNS — Ethereum Push Notification Service) is a decentralized notification layer for Web3. Users subscribe to a channel through a wallet signature, receive push notifications in the app, email, or mobile phone. For a dApp, this solves the problem of "user doesn't know what's happening with their position" without requiring email storage.
Push Protocol architecture
Push works at three levels:
- On-chain part — a contract on Ethereum mainnet (and Polygon) records channel creation and user subscriptions
- Push nodes — a decentralized network of nodes that deliver notifications
-
SDK —
@pushprotocol/restapiand@pushprotocol/socketfor interaction
A channel is a smart contract or EOA with the right to send notifications to its subscribers. Channel creation requires staking 50 PUSH tokens.
Integration into dApp frontend
User channel subscription
import { PushAPI, CONSTANTS } from '@pushprotocol/restapi'
import { useWalletClient } from 'wagmi'
export function useChannelSubscription(channelAddress: string) {
const { data: walletClient } = useWalletClient()
const subscribe = async () => {
if (!walletClient) return
const pushUser = await PushAPI.initialize(walletClient, {
env: CONSTANTS.ENV.PROD,
})
await pushUser.notification.subscribe(
`eip155:1:${channelAddress}`, // CAIP-10 format
)
}
const checkSubscription = async (userAddress: string) => {
const subscriptions = await PushAPI.user.getSubscriptions({
user: `eip155:1:${userAddress}`,
env: CONSTANTS.ENV.PROD,
})
return subscriptions.some((s: any) =>
s.channel.toLowerCase() === channelAddress.toLowerCase()
)
}
return { subscribe, checkSubscription }
}
Subscription requires a transaction signature (or EIP-712 signature for gasless via Push relay). Users don't need to pay gas for most operations — Push supports gasless subscriptions via meta-transactions.
Display notifications in UI
import { PushAPI, CONSTANTS } from '@pushprotocol/restapi'
export function useNotifications(userAddress: string) {
const [notifications, setNotifications] = useState([])
useEffect(() => {
const fetchNotifications = async () => {
const notifs = await PushAPI.user.getFeeds({
user: `eip155:1:${userAddress}`,
env: CONSTANTS.ENV.PROD,
limit: 20,
})
setNotifications(notifs)
}
fetchNotifications()
}, [userAddress])
return notifications
}
Real-time via WebSocket
import { createSocketConnection, CONSTANTS } from '@pushprotocol/socket'
const pushSocket = createSocketConnection({
user: `eip155:1:${userAddress}`,
env: CONSTANTS.ENV.PROD,
socketOptions: { autoConnect: false },
})
pushSocket.connect()
pushSocket.on(CONSTANTS.SOCKET.EVENTS.USER_FEEDS, (notification) => {
// Show toast notification
showToast({
title: notification.payload.notification.title,
body: notification.payload.notification.body,
})
})
Sending notifications from the backend
Notifications about on-chain events are sent from the server: monitor events through RPC/webhook, and when an event occurs, send a notification through the Push SDK.
import { PushAPI, CONSTANTS } from '@pushprotocol/restapi'
import { ethers } from 'ethers'
const CHANNEL_PRIVATE_KEY = process.env.PUSH_CHANNEL_PRIVATE_KEY!
async function sendLiquidationWarning(userAddress: string, healthFactor: number) {
const signer = new ethers.Wallet(CHANNEL_PRIVATE_KEY)
const pushUser = await PushAPI.initialize(signer, {
env: CONSTANTS.ENV.PROD,
})
await pushUser.channel.send([`eip155:1:${userAddress}`], {
notification: {
title: 'Liquidation Warning',
body: `Health factor dropped to ${healthFactor.toFixed(2)}. Add collateral.`,
},
payload: {
title: 'Liquidation Warning',
body: `Health factor: ${healthFactor.toFixed(2)}`,
cta: 'https://yourdapp.com/positions',
category: CONSTANTS.NOTIFICATION.TYPE.TARGETED,
},
})
}
Monitoring on-chain events
Push Protocol is a delivery layer, but you need to implement event triggering yourself. Monitoring patterns:
Polling via RPC — a cron job polls getLogs or readContract every N seconds. Simple, reliable, but latency equals the polling interval.
Alchemy Notify / QuickNode Streams — webhook on on-chain event. Minimal latency (~1 block), but vendor lock-in.
WebSocket subscriptions — eth_subscribe via WSS endpoint, handle events in real-time:
const wsClient = createPublicClient({
chain: mainnet,
transport: webSocket(process.env.ALCHEMY_WSS_URL),
})
wsClient.watchContractEvent({
address: lendingPoolAddress,
abi: lendingPoolAbi,
eventName: 'HealthFactorUpdated',
onLogs: async (logs) => {
for (const log of logs) {
const { user, healthFactor } = log.args
if (healthFactor < parseUnits('1.1', 18)) {
await sendLiquidationWarning(user, Number(formatUnits(healthFactor, 18)))
}
}
},
})
Typical use cases
- DeFi: liquidation warnings (health factor < threshold)
- DEX: limit order execution
- NFT: bid on your token, floor price changes
- DAO: new proposal for voting
- Bridge: transaction completed on another network
- Yield: APY dropped below target
Integrating Push Protocol into an existing dApp with several notification types — 2–3 days. Includes: creating a channel in Push, setting up on-chain event monitoring, subscription component in UI, and real-time WebSocket notifications.







