Skip to content
Last updated

Events & transaction monitoring

When a transaction occurs on a blockchain, it is rarely a single action. An EVM transaction might transfer ETH, pay a gas fee, trigger a token approval, and call a smart contract - all in one atomic operation. If your system only tracks "money moved from A to B," you are missing most of what happened.

Vilna breaks every transaction into individual canonical events - one event per meaningful action. This page explains the event model, what each event type means, how notifications work, and how to build on top of them. For real-world examples of event monitoring in action, see the use cases.

What are events

A blockchain transaction is a container. Inside it, multiple things happen: value moves, fees are paid, permissions are granted, contracts are called. Vilna parses each transaction and extracts every discrete action into a separate event object.

Each event has:

  • kind - the type of action (transfer, utxo_transfer, fee, approval, call, contract_create)
  • sequence - a 0-based position within the transaction, so you know the order of operations
  • data - a payload specific to the event kind, containing addresses, amounts, and asset identifiers

A simple ETH transfer produces two events: a transfer and a fee. A DeFi swap might produce five or more: a call to the router contract, multiple transfer events as tokens move through liquidity pools, and a fee event for gas.

This granularity means your system can react to exactly what happened, not just approximate it.

Event types

Vilna normalizes all on-chain activity into canonical event types. Regardless of which blockchain a transaction occurs on, the events always follow the same structure.

Transaction

transfer
assets moved

utxo_transfer
UTXO inputs/outputs

fee
gas paid

approval
spend permission

call
contract invoked

contract_create
new contract

Transaction

transfer
assets moved

utxo_transfer
UTXO inputs/outputs

fee
gas paid

approval
spend permission

call
contract invoked

contract_create
new contract

transfer

Assets moved from one address to another.

FieldDescription
asset_gidCAIP-19 identifier of the transferred asset
fromSender address
toRecipient address
amountTransfer amount (base + formatted)
sourceOrigin of the transfer: native (chain-level), contract (token contract), or internal (internal transaction)
token_idToken identifier for NFTs (ERC-721/ERC-1155), if applicable

Example scenario: A user sends 1,000 USDT to a merchant wallet. Vilna emits a transfer event with the sender, recipient, token, and exact amount. Your deposit detection system matches the recipient to a customer and credits their account.

{
  "kind": "transfer",
  "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
  "from": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
  "to": "0x8ba1f109551bd432803012645ac136c0d1e5c400",
  "amount": { "base": "1000000000", "formatted": "1000.0" },
  "source": "contract"
}

utxo_transfer

A UTXO-based transfer with full input/output visibility. Used for Bitcoin and other UTXO chains instead of transfer, providing complete details of all funding sources and all destinations in a single event.

FieldDescription
asset_gidCAIP-19 identifier of the transferred asset
inputsArray of transaction inputs, each with address, amount (base + formatted), and index
outputsArray of transaction outputs, each with address, amount (base + formatted), and index
feeTransaction fee (base + formatted)

Example scenario: A user consolidates two UTXOs into a single payment. The utxo_transfer event shows both input addresses (funding sources), the recipient output, the change output, and the miner fee - all in one event.

{
  "kind": "utxo_transfer",
  "asset_gid": "bip122:000000000019d6689c085ae165831e93/slip44:0",
  "inputs": [
    { "address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "amount": { "base": "50000", "formatted": "0.0005" }, "index": 0 },
    { "address": "bc1q5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8", "amount": { "base": "30000", "formatted": "0.0003" }, "index": 1 }
  ],
  "outputs": [
    { "address": "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3s7p", "amount": { "base": "70000", "formatted": "0.0007" }, "index": 0 },
    { "address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "amount": { "base": "8000", "formatted": "0.00008" }, "index": 1 }
  ],
  "fee": { "base": "2000", "formatted": "0.00002" }
}

This structure is important for compliance workflows - AML screening requires checking all input addresses (funding sources), not just a single sender.

fee

A network fee was paid to process the transaction.

FieldDescription
asset_gidCAIP-19 identifier of the fee asset (always the chain's native currency)
payerAddress that paid the fee
amountFee amount (base + formatted)

Example scenario: Every Ethereum transaction costs gas. Your accounting system needs to track fee expenditure separately from transfers to calculate true cost per transaction. The fee event gives you the exact amount paid, without manual calculation.

{
  "kind": "fee",
  "asset_gid": "eip155:1/slip44:60",
  "payer": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
  "amount": { "base": "2100000000000000", "formatted": "0.0021" }
}

approval

A token spending permission was granted or changed.

FieldDescription
asset_gidCAIP-19 identifier of the approved token
ownerAddress granting the permission
spenderAddress receiving the permission to spend
amountApproved spending limit (base + formatted)
token_idToken identifier for NFT approvals (ERC-721/ERC-1155), if applicable

Example scenario: A user approves a DEX router to spend their USDT. For a custodial platform, this is a security-critical event - someone just authorized a third party to move tokens from a monitored address. Your compliance system should flag and review unlimited approvals.

{
  "kind": "approval",
  "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
  "owner": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
  "spender": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
  "amount": { "base": "115792089237316195423570985008687907853269984665640564039457584007913129639935", "formatted": "unlimited" }
}

call

A smart contract function was executed.

FieldDescription
callerAddress that initiated the call
targetContract address that was called

Example scenario: A monitored address interacts with an unknown smart contract. The call event tells your risk system that a contract interaction occurred, even if no tokens moved directly. This is important for detecting phishing contracts, tracking DeFi positions, and maintaining complete audit trails.

{
  "kind": "call",
  "caller": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
  "target": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
}

contract_create

A new smart contract was deployed.

FieldDescription
creatorAddress that deployed the contract
created_addressAddress of the newly deployed contract

Example scenario: One of your monitored addresses deploys a new smart contract. For compliance teams, contract deployment is a significant action that should be logged and reviewed. The contract_create event captures exactly who deployed what and where.

{
  "kind": "contract_create",
  "creator": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
  "created_address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
}

Why track crypto transactions beyond transfers

Many blockchain monitoring solutions only track token transfers. Vilna tracks all canonical event types because real-world use cases demand it.

Security

An approval event means someone authorized a third party to spend tokens from a monitored address. For a crypto bank, exchange, or custodial wallet, this is a security-critical event. If an attacker gains access to one of your addresses and grants themselves an unlimited token approval, a transfer-only system would not notice until the tokens are actually moved - by which time it is too late. Monitoring approvals gives you an early warning window.

Compliance

Regulators expect complete audit trails. A transfer-only log misses contract interactions that move value in indirect ways - liquidity pool deposits, staking operations, and DAO governance calls all affect your exposure without producing a simple "A sent X to B" event. The call and contract_create events fill these gaps.

DeFi visibility

In DeFi, value moves through smart contract calls, not just simple transfers. A user might call a swap router that then triggers three internal transfers across two liquidity pools. Without the call event, you see tokens appearing and disappearing with no explanation. With it, you have the full context: which contract was called, which address initiated it, and what transfers resulted.

Multi-chain consistency

Different blockchains represent the same actions differently. Ethereum uses ERC-20 Transfer events; Tron uses TRC-20 events; Bitcoin uses UTXO inputs and outputs. Vilna normalizes everything into the same canonical event types. Your code handles an Ethereum transfer and a Bitcoin transfer with the same approach - consistent field naming and predictable structure across chains.

Complete picture

A single transaction can contain many events. Consider a user swapping tokens on a DEX:

BlockchainLiquidity PoolDEX RouterUserBlockchainLiquidity PoolDEX RouterUser1 transaction → 4 events1. call (swap)2. transfer (input tokens)3. transfer (output tokens)4. fee (gas)
BlockchainLiquidity PoolDEX RouterUserBlockchainLiquidity PoolDEX RouterUser1 transaction → 4 events1. call (swap)2. transfer (input tokens)3. transfer (output tokens)4. fee (gas)
  1. call - the user calls the swap router contract
  2. transfer - the user's input tokens move to the liquidity pool
  3. transfer - the output tokens move from the pool to the user
  4. fee - the gas fee is paid

If you only tracked transfers, you would see tokens coming and going but miss the contract interaction that caused them. You would also miss the fee, making it impossible to calculate the true cost of the operation.

What is tracked per chain family

Different blockchains have different capabilities. Here is what Vilna tracks for each chain family:

CapabilityEVMBitcoinTRONSolana
transferNative + ERC-20 tokensN/ANative + TRC-20 tokensNative + SPL tokens
utxo_transferN/ABTC (full UTXO inputs/outputs)N/AN/A
feeGas feesMiner feesBandwidth/energy feesTransaction fees
approvalERC-20 approvalsN/ATRC-20 approvalsN/A
callSmart contract callsN/ASmart contract callsProgram interactions
contract_createContract deploymentN/AContract deploymentProgram deployment

EVM chains (Ethereum, Polygon, Arbitrum, BSC, etc.) support transfer, fee, approval, call, and contract_create, including internal transactions and ERC-721/ERC-1155 NFT events.

Bitcoin supports utxo_transfer and fee events. Bitcoin does not have smart contracts in the EVM sense, so approval, call, and contract_create do not apply. Bitcoin uses utxo_transfer instead of transfer to provide full visibility into all inputs (funding sources) and outputs (destinations) - this is essential for AML compliance and accurate accounting.

TRON is similar to EVM - it supports transfer, fee, approval, call, and contract_create for TRC-20 tokens and smart contract interactions.

Solana supports transfer and fee events, with program-level interaction tracking for call events.

The complete and current list of supported chains and their capabilities is available via GET /blockchains in the Platform API.

Notification lifecycle

When a transaction touches one of your monitored addresses, Vilna sends notifications at specific lifecycle stages. Understanding this lifecycle is essential for building reliable systems.

The flow

Your SystemVilnaBlockchainYour SystemVilnaBlockchainloop[Confirmations]New blockParse transactionsExtract canonical eventsMatch monitored addressestransaction_detected (fast, not final)Block +1, +2, ... +NCount confirmationstransaction_confirmed (reliable)
Your SystemVilnaBlockchainYour SystemVilnaBlockchainloop[Confirmations]New blockParse transactionsExtract canonical eventsMatch monitored addressestransaction_detected (fast, not final)Block +1, +2, ... +NCount confirmationstransaction_confirmed (reliable)
  1. A transaction is included in a block on the blockchain.
  2. Vilna detects the block, parses every transaction, and extracts canonical events.
  3. Vilna checks if any monitored addresses are involved in the transaction's events or activity.
  4. If a match is found, a transaction_detected notification is sent via all configured channels (webhook and Telegram). The transaction's block status is processed (detected but not yet confirmed).
  5. As new blocks are added on top, the transaction accumulates confirmations.
  6. When the required confirmation count is reached, a transaction_confirmed notification is sent. The block status is now confirmed.
  7. If a blockchain reorganization occurs (rare, but possible), the block status transitions to reorged. The block is reprocessed from the new chain state.

Detection vs. confirmation

Notifications are sent at two key moments, and your system should handle them differently:

Event typeBlock statusSpeedFinalityUse for
transaction_detectedprocessedFast (seconds after block)Not final - could be reorgedUI updates, provisional display, alerting humans
transaction_confirmedconfirmedSlower (minutes, depends on chain)Final - safe to act onCrediting accounts, triggering business logic, updating balances

Design your system around this distinction. Show detected transactions in your UI immediately so users see responsive feedback. But do not credit accounts, trigger payouts, or update ledgers until you receive the confirmation notification. This two-phase approach gives you both speed and safety.

Do not act on detected events

Show detected alerts (block status processed) in your UI for responsiveness, but never credit accounts, trigger payouts, or update ledgers until you receive a confirmed alert (block status confirmed).

Webhook notifications in practice

When a notification fires, your webhook receives a TransactionAlertPayload - a self-contained JSON object with everything you need to process the event without making additional API calls.

Payload structure

The payload has four top-level fields:

FieldDescription
event_idUUIDv5 matching the X-Webhook-Event-Id header. Stable across retries — use it as your deduplication key.
itemThe full transaction object, including all events and activity records
referencesLookup maps for tokens, blockchains, and addresses referenced in the transaction
is_test_messagetrue if sent by the test action, false for real blockchain events

Example payload

Here is a realistic webhook payload for a 1,000 USDT transfer on Ethereum. The transaction contains two events (the token transfer and the gas fee) and one activity record showing the net impact on the monitored address:

{
  "event_id": "f47ac10b-58cc-5372-a567-0e02b2c3d479",
  "item": {
    "chain_gid": "eip155:1",
    "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "block_number": 19500000,
    "is_success": true,
    "confirmed_at": "2025-03-15T10:30:00Z",
    "events": [
      {
        "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "sequence": 0,
        "kind": "transfer",
        "data": {
          "kind": "transfer",
          "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
          "from": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
          "to": "0x8ba1f109551bd432803012645ac136c0d1e5c400",
          "amount": { "base": "1000000000", "formatted": "1000.0" },
          "source": "contract"
        }
      },
      {
        "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "sequence": 1,
        "kind": "fee",
        "data": {
          "kind": "fee",
          "asset_gid": "eip155:1/slip44:60",
          "payer": "0x742d35cc6634c0532925a3b844Bc9e7595f7B123",
          "amount": { "base": "2100000000000000", "formatted": "0.0021" }
        }
      }
    ],
    "activity": [
      {
        "chain_gid": "eip155:1",
        "txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "address": "0x8ba1f109551bd432803012645ac136c0d1e5c400",
        "asset_gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
        "direction": "in",
        "delta": { "base": "1000000000", "formatted": "1000.0" },
        "created_at": "2025-03-15T10:30:00Z"
      }
    ]
  },
  "references": {
    "tokens": {
      "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7": {
        "gid": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7",
        "name": "Tether USD",
        "symbol": "USDT",
        "decimals": 6
      },
      "eip155:1/slip44:60": {
        "gid": "eip155:1/slip44:60",
        "name": "Ethereum",
        "symbol": "ETH",
        "decimals": 18
      }
    },
    "blockchains": {
      "eip155:1": {
        "gid": "eip155:1",
        "name": "ethereum",
        "short_name": "eth"
      }
    },
    "addresses": {
      "0x8ba1f109551bd432803012645ac136c0d1e5c400": "merchant_wallet"
    }
  },
  "is_test_message": false
}

Key things to notice:

  • The events array contains every canonical event in the transaction. You can filter by kind to process only the event types you care about.
  • The activity array shows the net balance impact on your monitored addresses. Each entry tells you which address was affected, which asset changed, the direction (in or out), and the exact delta.
  • The references object lets you resolve token symbols, names, decimals, and blockchain metadata without calling the API. Everything you need for display and processing is in the payload.

Routing notifications beyond webhooks

Vilna delivers notifications natively to webhooks and Telegram. But a webhook is just an HTTP POST - you can route that data anywhere your architecture needs it.

Message queues - Point your webhook URL at a lightweight proxy that enqueues messages into RabbitMQ, AWS SQS, or Google Pub/Sub. This decouples event ingestion from processing and gives you backpressure handling for free.

Slack / Discord - Your webhook handler can reformat the payload and forward it to Slack or Discord incoming webhook URLs. This is useful for operations teams who want human-readable transaction alerts in their chat channels.

PagerDuty / OpsGenie - Forward high-value events (large transfers, unexpected approvals, contract deployments) to alerting systems for on-call escalation.

Custom backends - Any system that accepts HTTP POST can receive Vilna events. Point the webhook at your internal API, a serverless function, or a data pipeline ingestion endpoint.

Native adapters for Slack, Discord, and popular message queues are in development. If you need a specific integration, contact support@vilna.io.

Webhook delivery and retries

When Vilna sends a webhook notification, it expects your endpoint to return a 2xx status code within 10 seconds. A delivery is considered failed if the connection cannot be established, the response times out, or the endpoint returns a 4xx/5xx status code.

Failed deliveries are retried on the following schedule:

AttemptDelay
1Immediate
230 seconds
32 minutes
410 minutes
530 minutes

If all five attempts fail, the event is marked as undelivered. A single undelivered event does not disable the channel - transient failures are expected. However, if an endpoint fails repeatedly across multiple events, Vilna disables the channel to stop wasting resources. A 410 Gone response immediately disables the channel on the first attempt.

Delivery ordering: Notifications are dispatched in the order events occur, but delivery is not strictly ordered. If your endpoint is slow to respond, a later event may arrive before an earlier retry completes. Use block_number and sequence from the payload to reconstruct the correct order on your side.

Designing your webhook handler

A well-built webhook handler is reliable, idempotent, and fast. Follow these practices to avoid common pitfalls.

Respond immediately, process later

Your webhook endpoint should return HTTP 2xx as fast as possible. Enqueue the payload for asynchronous processing rather than doing heavy work in the request handler.

Best practice

Return HTTP 200 immediately and enqueue the payload for async processing. Heavy work in the request handler risks timeouts and missed deliveries.

POST

200 OK

Vilna

Your Endpoint

Queue

Async Worker

Business Logic

POST

200 OK

Vilna

Your Endpoint

Queue

Async Worker

Business Logic

Verify the signature

Every delivery carries an X-Webhook-Signature header in Stripe format (t=<unix-seconds>,v1=<hex>) with an HMAC-SHA256 of the raw request body. Verify it before processing — both to confirm the payload came from Vilna and to reject replays via the timestamp inside the header. See Authentication — Webhook signature verification for the verification function and required client behaviors.

Deduplicate by event_id

Use event_id from the body (or X-Webhook-Event-Id from the headers — they are the same value) to detect duplicate deliveries. Store processed IDs in a database or cache and skip any you have already seen. This is critical for financial operations — you never want to credit an account twice for the same transaction.

Filter by event kind

If your system only cares about transfers, filter the events array by kind and ignore the rest. For EVM/TRON/Solana chains, look for transfer; for Bitcoin, look for utxo_transfer. This keeps your processing logic focused:

const transfers = payload.item.events.filter(
  (event) => event.kind === "transfer" || event.kind === "utxo_transfer"
);

for (const event of transfers) {
  if (event.kind === "utxo_transfer") {
    // Bitcoin: full UTXO inputs/outputs
    await processUtxoTransfer(event.data);
  } else {
    // EVM/TRON/Solana: single from→to transfer
    await processTransfer(event.data);
  }
}

Use the references object

The references field contains token metadata (name, symbol, decimals) and blockchain information for every asset and chain mentioned in the transaction. Use this data directly instead of making extra API calls:

const tokenGid = event.data.asset_gid;
const token = payload.references.tokens[tokenGid];
console.log(`Received ${event.data.amount.formatted} ${token.symbol}`);

Summary of webhook headers

HeaderPurpose
X-Webhook-SignatureStripe-format HMAC-SHA256 signature: t=<unix-seconds>,v1=<hex>
X-Webhook-EventEvent name in dot notation: transaction.detected or transaction.confirmed
X-Webhook-Event-IdUUIDv5, stable across retries — use as dedup key
X-Webhook-Delivery-IdUUID, changes on every attempt — for support correlation only

Further reading