Build a real-time deposit alert system that sends Discord notifications whenever stablecoins are deposited into a Treasury Vault
Monitoring treasury deposits and token transfers in real time is critical for DAOs, DeFi projects, and teams that manage on-chain funds. Traditionally, that requires backend servers and cron jobs: infrastructure you must maintain and secure.Kwala Workflows removes that operational burden. With a simple YAML-driven workflow, you can listen for on-chain events and send notifications to off-chain endpoints like Discord, no backend required.In this guide, we’ll build a real-time deposit alert system that sends a Discord notification whenever a stablecoin (USDC) is deposited into a Treasury Vault contract. The entire flow is driven by Kwala workflows and a single YAML configuration.
This use case uses two Solidity contracts: a simple ERC20 (USDC-like) and a treasury vault that accepts deposits and emits an event.MintERC20USDC.solContract Address (Base Sepolia): 0x025B55313ac120435963f571F02EcC3d35FA6e6e
Copy
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;/// @title Simple USDC-like ERC20 (standalone)/// @notice Minimal ERC20 token with mint/burn and an owner-only function to transfer tokens held by the contract.contract USDC { // ERC20 basic data string public name = "USD Coin"; string public symbol = "USDC"; uint8 public constant decimals = 6; // USDC uses 6 decimals uint256 public totalSupply; // Owner address public owner; // Balances and allowances mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; // Events event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed ownerAddr, address indexed spender, uint256 value); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event Mint(address indexed to, uint256 amount); event Burn(address indexed from, uint256 amount); // Modifiers modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; } // Constructor sets deployer as owner constructor() { owner = msg.sender; emit OwnershipTransferred(address(0), owner); } // ---------- ERC20 standard functions ---------- function transfer(address to, uint256 amount) external returns (bool) { _transfer(msg.sender, to, amount); return true; } function approve(address spender, uint256 amount) external returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function transferFrom(address from, address to, uint256 amount) external returns (bool) { uint256 allowed = allowance[from][msg.sender]; require(allowed >= amount, "allowance too low"); // deduct allowance allowance[from][msg.sender] = allowed - amount; _transfer(from, to, amount); return true; } // ---------- Internal transfer ---------- function _transfer(address from, address to, uint256 amount) internal { require(to != address(0), "transfer to zero"); uint256 bal = balanceOf[from]; require(bal >= amount, "insufficient balance"); balanceOf[from] = bal - amount; balanceOf[to] += amount; emit Transfer(from, to, amount); } // ---------- Mint / Burn (owner) ---------- /// @notice Mint tokens to an address (owner only) function mint(address to, uint256 amount) external onlyOwner returns (bool) { require(to != address(0), "mint to zero"); totalSupply += amount; balanceOf[to] += amount; emit Mint(to, amount); emit Transfer(address(0), to, amount); return true; } /// @notice Burn tokens from owner's balance function burn(uint256 amount) external onlyOwner returns (bool) { uint256 bal = balanceOf[owner]; require(bal >= amount, "insufficient owner balance"); balanceOf[owner] = bal - amount; totalSupply -= amount; emit Burn(owner, amount); emit Transfer(owner, address(0), amount); return true; } /// @notice Burn tokens from any account (owner only) function burnFrom(address account, uint256 amount) external onlyOwner returns (bool) { require(account != address(0), "burn from zero"); uint256 bal = balanceOf[account]; require(bal >= amount, "insufficient balance"); balanceOf[account] = bal - amount; totalSupply -= amount; emit Burn(account, amount); emit Transfer(account, address(0), amount); return true; } // ---------- Contract-held tokens forwarding ---------- /// @notice Transfer tokens that this contract currently holds to `to` (owner only). /// Useful if the contract receives tokens and owner wants to forward them to their wallet. function transferFromContract(address to, uint256 amount) external onlyOwner returns (bool) { require(to != address(0), "to zero"); uint256 bal = balanceOf[address(this)]; require(bal >= amount, "insufficient contract balance"); balanceOf[address(this)] = bal - amount; balanceOf[to] += amount; emit Transfer(address(this), to, amount); return true; } // ---------- Ownership ---------- function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "new owner zero"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } // ---------- Allow contract to receive ETH (optional) ---------- receive() external payable { }}
Name: USDC_Received_Notifier1_e81eTrigger: TriggerSourceContract: NA TriggerChainID: NA TriggerEventName: NA TriggerEventFilter: NA TriggerSourceContractABI: NA TriggerPrice: NA RecurringSourceContract: 0x60Fc086C786e8B66aeD0163A266bd3aF9125e8D0 RecurringChainID: 84532 RecurringEventName: USDCAccepted(address,uint256,uint256) RecurringEventFilter: NA RecurringSourceContractABI: WwogIHsKICAgICJpbnB1dHMiOiBbCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogImFkZHJlc3MiLAogICAgICAgICJuYW1lIjogIl91c2RjIiwKICAgICAgICAidHlwZSI6ICJhZGRyZXNzIgogICAgICB9CiAgICBdLAogICAgInN0YXRlTXV0YWJpbGl0eSI6ICJub25wYXlhYmxlIiwKICAgICJ0eXBlIjogImNvbnN0cnVjdG9yIgogIH0sCiAgewogICAgImFub255bW91cyI6IGZhbHNlLAogICAgImlucHV0cyI6IFsKICAgICAgewogICAgICAgICJpbmRleGVkIjogdHJ1ZSwKICAgICAgICAiaW50ZXJuYWxUeXBlIjogImFkZHJlc3MiLAogICAgICAgICJuYW1lIjogInNlbmRlciIsCiAgICAgICAgInR5cGUiOiAiYWRkcmVzcyIKICAgICAgfSwKICAgICAgewogICAgICAgICJpbmRleGVkIjogZmFsc2UsCiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICJhbW91bnQiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiaW5kZXhlZCI6IGZhbHNlLAogICAgICAgICJpbnRlcm5hbFR5cGUiOiAidWludDI1NiIsCiAgICAgICAgIm5hbWUiOiAidGltZXN0YW1wIiwKICAgICAgICAidHlwZSI6ICJ1aW50MjU2IgogICAgICB9CiAgICBdLAogICAgIm5hbWUiOiAiVVNEQ0FjY2VwdGVkIiwKICAgICJ0eXBlIjogImV2ZW50IgogIH0sCiAgewogICAgImlucHV0cyI6IFtdLAogICAgIm5hbWUiOiAiY29udHJhY3RCYWxhbmNlIiwKICAgICJvdXRwdXRzIjogWwogICAgICB7CiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICIiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0KICAgIF0sCiAgICAic3RhdGVNdXRhYmlsaXR5IjogInZpZXciLAogICAgInR5cGUiOiAiZnVuY3Rpb24iCiAgfSwKICB7CiAgICAiaW5wdXRzIjogWwogICAgICB7CiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICJhbW91bnQiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0KICAgIF0sCiAgICAibmFtZSI6ICJkZXBvc2l0IiwKICAgICJvdXRwdXRzIjogW10sCiAgICAic3RhdGVNdXRhYmlsaXR5IjogIm5vbnBheWFibGUiLAogICAgInR5cGUiOiAiZnVuY3Rpb24iCiAgfSwKICB7CiAgICAiaW5wdXRzIjogWwogICAgICB7CiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICIiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0KICAgIF0sCiAgICAibmFtZSI6ICJkZXBvc2l0cyIsCiAgICAib3V0cHV0cyI6IFsKICAgICAgewogICAgICAgICJpbnRlcm5hbFR5cGUiOiAiYWRkcmVzcyIsCiAgICAgICAgIm5hbWUiOiAic2VuZGVyIiwKICAgICAgICAidHlwZSI6ICJhZGRyZXNzIgogICAgICB9LAogICAgICB7CiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICJhbW91bnQiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogInVpbnQyNTYiLAogICAgICAgICJuYW1lIjogInRpbWVzdGFtcCIsCiAgICAgICAgInR5cGUiOiAidWludDI1NiIKICAgICAgfQogICAgXSwKICAgICJzdGF0ZU11dGFiaWxpdHkiOiAidmlldyIsCiAgICAidHlwZSI6ICJmdW5jdGlvbiIKICB9LAogIHsKICAgICJpbnB1dHMiOiBbXSwKICAgICJuYW1lIjogImRlcG9zaXRzQ291bnQiLAogICAgIm91dHB1dHMiOiBbCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogInVpbnQyNTYiLAogICAgICAgICJuYW1lIjogIiIsCiAgICAgICAgInR5cGUiOiAidWludDI1NiIKICAgICAgfQogICAgXSwKICAgICJzdGF0ZU11dGFiaWxpdHkiOiAidmlldyIsCiAgICAidHlwZSI6ICJmdW5jdGlvbiIKICB9LAogIHsKICAgICJpbnB1dHMiOiBbCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogInVpbnQyNTYiLAogICAgICAgICJuYW1lIjogImluZGV4IiwKICAgICAgICAidHlwZSI6ICJ1aW50MjU2IgogICAgICB9CiAgICBdLAogICAgIm5hbWUiOiAiZ2V0RGVwb3NpdCIsCiAgICAib3V0cHV0cyI6IFsKICAgICAgewogICAgICAgICJpbnRlcm5hbFR5cGUiOiAiYWRkcmVzcyIsCiAgICAgICAgIm5hbWUiOiAiIiwKICAgICAgICAidHlwZSI6ICJhZGRyZXNzIgogICAgICB9LAogICAgICB7CiAgICAgICAgImludGVybmFsVHlwZSI6ICJ1aW50MjU2IiwKICAgICAgICAibmFtZSI6ICIiLAogICAgICAgICJ0eXBlIjogInVpbnQyNTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogInVpbnQyNTYiLAogICAgICAgICJuYW1lIjogIiIsCiAgICAgICAgInR5cGUiOiAidWludDI1NiIKICAgICAgfQogICAgXSwKICAgICJzdGF0ZU11dGFiaWxpdHkiOiAidmlldyIsCiAgICAidHlwZSI6ICJmdW5jdGlvbiIKICB9LAogIHsKICAgICJpbnB1dHMiOiBbXSwKICAgICJuYW1lIjogInRvdGFsUmVjZWl2ZWQiLAogICAgIm91dHB1dHMiOiBbCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogInVpbnQyNTYiLAogICAgICAgICJuYW1lIjogIiIsCiAgICAgICAgInR5cGUiOiAidWludDI1NiIKICAgICAgfQogICAgXSwKICAgICJzdGF0ZU11dGFiaWxpdHkiOiAidmlldyIsCiAgICAidHlwZSI6ICJmdW5jdGlvbiIKICB9LAogIHsKICAgICJpbnB1dHMiOiBbCiAgICAgIHsKICAgICAgICAiaW50ZXJuYWxUeXBlIjogImFkZHJlc3MiLAogICAgICAgICJuYW1lIjogIiIsCiAgICAgICAgInR5cGUiOiAiYWRkcmVzcyIKICAgICAgfQogICAgXSwKICAgICJuYW1lIjogInRvdGFsUmVjZWl2ZWRCeSIsCiAgICAib3V0cHV0cyI6IFsKICAgICAgewogICAgICAgICJpbnRlcm5hbFR5cGUiOiAidWludDI1NiIsCiAgICAgICAgIm5hbWUiOiAiIiwKICAgICAgICAidHlwZSI6ICJ1aW50MjU2IgogICAgICB9CiAgICBdLAogICAgInN0YXRlTXV0YWJpbGl0eSI6ICJ2aWV3IiwKICAgICJ0eXBlIjogImZ1bmN0aW9uIgogIH0sCiAgewogICAgImlucHV0cyI6IFtdLAogICAgIm5hbWUiOiAidXNkYyIsCiAgICAib3V0cHV0cyI6IFsKICAgICAgewogICAgICAgICJpbnRlcm5hbFR5cGUiOiAiYWRkcmVzcyIsCiAgICAgICAgIm5hbWUiOiAiIiwKICAgICAgICAidHlwZSI6ICJhZGRyZXNzIgogICAgICB9CiAgICBdLAogICAgInN0YXRlTXV0YWJpbGl0eSI6ICJ2aWV3IiwKICAgICJ0eXBlIjogImZ1bmN0aW9uIgogIH0KXQ== RecurringPrice: NA RepeatEvery: event ExecuteAfter: immediate ExpiresIn: 1759915800 Meta: NA ActionStatusNotificationPOSTURL: ActionStatusNotificationAPIKey: NAActions: - Name: discord_call Type: post APIEndpoint: https://discord.com/api/webhooks/1425037499574517760/wh9zok5wxXMI-rdVK-4atTZM-14eHghibVwbQN7erqs1iSME7keWQGulSZ6NNBVyaXA1 APIPayload: content: "USDC Received on USDC Treasury Vault Smart Contract" TargetContract: NA TargetFunction: NA TargetParams: NA ChainID: NA EncodedABI: NA Bytecode: NA Metadata: NA RetriesUntilSuccess: 5Execution: Mode: sequential
Paste this YAML into the Kwala dashboard, save & compile, then deploy.
You’ve built a real-time stablecoin deposit alert using Kwala Workflows, a single YAML workflow that connects on-chain USDCAccepted events to a Discord notification. No servers, no cron jobs, and no custom backend.Kwala helps you connect on-chain events to off-chain systems quickly, automate DeFi treasury monitoring without operational overhead, and build event-driven Web3 infrastructure using YAML.