# 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](/business/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.


```mermaid
flowchart LR
    TX[Transaction] --> T["transfer<br/>assets moved"]
    TX --> UT["utxo_transfer<br/>UTXO inputs/outputs"]
    TX --> F["fee<br/>gas paid"]
    TX --> A["approval<br/>spend permission"]
    TX --> C["call<br/>contract invoked"]
    TX --> CC["contract_create<br/>new contract"]
```

### transfer

Assets moved from one address to another.

| Field | Description |
|  --- | --- |
| `asset_gid` | CAIP-19 identifier of the transferred asset |
| `from` | Sender address |
| `to` | Recipient address |
| `amount` | Transfer amount (`base` + `formatted`) |
| `source` | Origin of the transfer: `native` (chain-level), `contract` (token contract), or `internal` (internal transaction) |
| `token_id` | Token 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.


```json
{
  "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.

| Field | Description |
|  --- | --- |
| `asset_gid` | CAIP-19 identifier of the transferred asset |
| `inputs` | Array of transaction inputs, each with `address`, `amount` (`base` + `formatted`), and `index` |
| `outputs` | Array of transaction outputs, each with `address`, `amount` (`base` + `formatted`), and `index` |
| `fee` | Transaction 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.


```json
{
  "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.

| Field | Description |
|  --- | --- |
| `asset_gid` | CAIP-19 identifier of the fee asset (always the chain's native currency) |
| `payer` | Address that paid the fee |
| `amount` | Fee 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.


```json
{
  "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.

| Field | Description |
|  --- | --- |
| `asset_gid` | CAIP-19 identifier of the approved token |
| `owner` | Address granting the permission |
| `spender` | Address receiving the permission to spend |
| `amount` | Approved spending limit (`base` + `formatted`) |
| `token_id` | Token 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.


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

### call

A smart contract function was executed.

| Field | Description |
|  --- | --- |
| `caller` | Address that initiated the call |
| `target` | Contract 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.


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

### contract_create

A new smart contract was deployed.

| Field | Description |
|  --- | --- |
| `creator` | Address that deployed the contract |
| `created_address` | Address 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.


```json
{
  "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:


```mermaid
sequenceDiagram
    participant U as User
    participant R as DEX Router
    participant LP as Liquidity Pool
    participant BC as Blockchain

    U->>R: 1. call (swap)
    R->>LP: 2. transfer (input tokens)
    LP->>U: 3. transfer (output tokens)
    U->>BC: 4. fee (gas)
    Note over U,BC: 1 transaction → 4 events
```

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:

| Capability | EVM | Bitcoin | TRON | Solana |
|  --- | --- | --- | --- | --- |
| `transfer` | Native + ERC-20 tokens | N/A | Native + TRC-20 tokens | Native + SPL tokens |
| `utxo_transfer` | N/A | BTC (full UTXO inputs/outputs) | N/A | N/A |
| `fee` | Gas fees | Miner fees | Bandwidth/energy fees | Transaction fees |
| `approval` | ERC-20 approvals | N/A | TRC-20 approvals | N/A |
| `call` | Smart contract calls | N/A | Smart contract calls | Program interactions |
| `contract_create` | Contract deployment | N/A | Contract deployment | Program 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`](/apis/platform/api/blockchain/list-blockchains) in the [Platform API](/apis/platform/).

## 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


```mermaid
sequenceDiagram
    participant BC as Blockchain
    participant V as Vilna
    participant S as Your System

    BC->>V: New block
    V->>V: Parse transactions
    V->>V: Extract canonical events
    V->>V: Match monitored addresses
    V->>S: transaction_alert (detected - fast, not final)
    loop Confirmations
        BC->>V: Block +1, +2, ... +N
        V->>V: Count confirmations
    end
    V->>S: transaction_alert (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_alert` 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, another `transaction_alert` 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:

| Notification | Block status | Speed | Finality | Use for |
|  --- | --- | --- | --- | --- |
| `transaction_alert` | `processed` (detected) | Fast (seconds after block) | Not final - could be reorged | UI updates, provisional display, alerting humans |
| `transaction_alert` | `confirmed` | Slower (minutes, depends on chain) | Final - safe to act on | Crediting 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 three top-level fields:

| Field | Description |
|  --- | --- |
| `item` | The full transaction object, including all events and activity records |
| `references` | Lookup maps for tokens, blockchains, and addresses referenced in the transaction |
| `is_test_message` | `true` 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:


```json
{
  "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](mailto: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:

| Attempt | Delay |
|  --- | --- |
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 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.


```mermaid
flowchart LR
    V[Vilna] -->|POST| E[Your Endpoint]
    E -->|200 OK| V
    E --> Q[Queue]
    Q --> W[Async Worker]
    W --> BL[Business Logic]
```

### Verify the signature

When webhook signature verification is available (planned - see [Authentication](/guides/authentication#webhook-signature-verification)), every delivery will include an `X-Webhook-Signature` header. Verify it before processing to ensure the payload was sent by Vilna and has not been tampered with.

Construct the signed payload by concatenating the `X-Webhook-Timestamp`, a dot (`.`), and the raw request body. Compute HMAC-SHA256 using your webhook secret and compare the result to the signature header. See [Authentication - Webhook Verification](/guides/authentication#webhook-signature-verification) for implementation examples.

### Deduplicate with the idempotency key

Use the `X-Webhook-Idempotency-Key` header to detect duplicate deliveries. Store processed keys in a database or cache and skip any key you have already seen. This is critical for financial operations - you never want to credit an account twice for the same transaction.

### Reject stale messages

Check `X-Webhook-Timestamp` against the current time. Reject events older than 5 minutes to prevent replay attacks.

### 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:


```typescript
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:


```typescript
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

| Header | Purpose |
|  --- | --- |
| `X-Webhook-Signature` | HMAC-SHA256 digest for payload verification (planned) |
| `X-Webhook-Timestamp` | Unix timestamp for replay attack prevention (planned) |
| `X-Webhook-Idempotency-Key` | Unique delivery ID for deduplication |
| `X-Webhook-Event` | Event type (`transaction_alert` or `test`) |


## Further reading

Notification Channels
Creating and managing webhook and Telegram channels

Authentication
Webhook signature verification with code examples

Core Concepts
Addresses, tokens, amounts, and the references pattern

Integration Patterns
Deposit detection, portfolio tracking, and transaction alerts