> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mpcvault.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TRON Watch Address

> Monitor TRON addresses you run yourself and receive signed webhooks for their transfers.

Watch Address monitors TRON addresses and notifies your server of their transfers - built for wallets you run yourself; both your own addresses and addresses on MPCVault can be registered. When a registered address sends or receives TRX or USDT on-chain, MPCVault POSTs a signed webhook to your configured URL.

* **Monitoring Only** - No wallet is created, MPCVault holds no keys, and no assets are moved
* **No Historical Backfill** - Only transfers that occur after registration are notified

<Note>
  Watch Address uses the same API token as Sweep, issued by MPCVault - not a token created from the web console. Contact your MPCVault account manager to enable it and configure your webhook URL.
</Note>

## Preparation

You provide:

* Webhook URL
* Outbound IPs (if IP allowlisting is enabled)

MPCVault provides:

* API base URL: `https://api.mpcvault.com`
* API token, sent in the `x-mtoken` header
* Webhook verification public key (see below)

## How It Works

1. Register each address via [AddWatchAddress](/api-reference/sweep/add-watch-address).
2. MPCVault sends a signed webhook for every transfer on the address, by lifecycle stage.
3. Verify the signature, deduplicate, and apply your business logic.

## Webhook

### Push Rules

* Both incoming and outgoing transfers are notified; there is no minimum amount
* Currently monitored assets: TRX and USDT
* Only successful on-chain transactions are pushed; failed transactions are not
* Self-transfers (sender and recipient are the same address) are not pushed
* Each transaction is notified by lifecycle stage: `CONFIRMED` (19 blocks) then `FINALIZED` (38 blocks), strictly in order; TRX and USDT use the same thresholds, and TRON does not send `REVERTED`

<Warning>
  Block thresholds are MPCVault's current operating standard, provided for reference only. They may change with network conditions and do not guarantee irreversibility: only `FINALIZED` is final. Crediting or releasing funds based on `CONFIRMED` is your own risk decision - MPCVault is not liable for losses caused by acting before `FINALIZED`. Apply stricter thresholds per your own risk requirements.
</Warning>

### Signature Verification

Request headers:

```
Content-Type: application/json
Signature: <base64_ed25519_signature>
Signature-Algorithm: Ed25519
```

Verification public key (Ed25519):

```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j
```

The signature covers the raw request body bytes, exactly as received - do not re-serialize the JSON before verifying:

```
Ed25519.Verify(public_key, raw_request_body, base64_decode(Signature))
```

<CodeGroup>
  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"crypto/ed25519"
  	"encoding/base64"
  	"strings"

  	"golang.org/x/crypto/ssh"
  )

  const publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j"

  func verify(signatureB64 string, rawBody []byte) bool {
  	parsed, _, _, rest, err := ssh.ParseAuthorizedKey([]byte(publicKey))
  	if err != nil ||
  		parsed.Type() != ssh.KeyAlgoED25519 ||
  		len(bytes.TrimSpace(rest)) != 0 {
  		return false
  	}

  	cryptoKey, ok := parsed.(ssh.CryptoPublicKey)
  	if !ok {
  		return false
  	}

  	pub, ok := cryptoKey.CryptoPublicKey().(ed25519.PublicKey)
  	if !ok || len(pub) != ed25519.PublicKeySize {
  		return false
  	}

  	signature, err := base64.StdEncoding.DecodeString(
  		strings.TrimSpace(signatureB64),
  	)
  	if err != nil || len(signature) != ed25519.SignatureSize {
  		return false
  	}

  	return ed25519.Verify(pub, rawBody, signature)
  }
  ```

  ```python Python theme={null}
  import base64

  from cryptography.exceptions import InvalidSignature
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
  from cryptography.hazmat.primitives.serialization import load_ssh_public_key

  PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j"

  public_key = load_ssh_public_key(PUBLIC_KEY.encode())
  assert isinstance(public_key, Ed25519PublicKey)


  def verify(signature_b64: str, raw_body: bytes) -> bool:
      try:
          public_key.verify(base64.b64decode(signature_b64), raw_body)
          return True
      except (InvalidSignature, ValueError):
          return False
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  const PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j";

  function parseSshEd25519(line) {
    const blob = Buffer.from(line.split(" ")[1], "base64");
    const algoLen = blob.readUInt32BE(0);
    const algo = blob.subarray(4, 4 + algoLen).toString();
    const keyLen = blob.readUInt32BE(4 + algoLen);
    const raw = blob.subarray(8 + algoLen, 8 + algoLen + keyLen);
    if (algo !== "ssh-ed25519" || raw.length !== 32) throw new Error("not an ssh-ed25519 key");
    return crypto.createPublicKey({
      key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]),
      format: "der",
      type: "spki",
    });
  }

  const publicKey = parseSshEd25519(PUBLIC_KEY);

  function verify(signatureB64, rawBody) {
    const signature = Buffer.from(signatureB64.trim(), "base64");
    if (signature.length !== 64) return false;
    return crypto.verify(null, rawBody, publicKey, signature);
  }
  ```
</CodeGroup>

### Request Body

Example (USDT incoming):

```json theme={null}
{
  "uuid": "3f0a7c9e-5b21-5c58-9e0b-2f6d8a4c1e77",
  "wallet_address": "TSvcVista6bxr7WFmDMWKLzTBnF4N34u9v",
  "network": "TRON",
  "unique_id": "tron:74123456:txhash:0",
  "block_number": 74123456,
  "block_hash": "0000000004...blockhash",
  "hash": "7c2b...transactionhash",
  "is_send": false,
  "from_address": "TPayerAddress...",
  "to_address": "TSvcVista6bxr7WFmDMWKLzTBnF4N34u9v",
  "mint_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  "decimals": 6,
  "amount": "100.25",
  "status": "SUCCESS",
  "event_type": "CONFIRMED",
  "timestamp": 1784515200000
}
```

| Field                                  | Description                                                                                            |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `uuid`                                 | Unique ID of this transaction record, identical across lifecycle stages                                |
| `wallet_address`                       | The registered watch address                                                                           |
| `network`                              | Always `TRON`                                                                                          |
| `unique_id`                            | On-chain event ID; shared by both notifications when both sides of a transfer are watched              |
| `block_number` / `block_hash` / `hash` | On-chain block number, block hash, transaction hash                                                    |
| `is_send`                              | `false` = incoming (watch address is recipient); `true` = outgoing (watch address is sender)           |
| `from_address` / `to_address`          | Sender / recipient; omitted when empty                                                                 |
| `mint_address`                         | Token contract address (USDT: `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`); empty for native TRX              |
| `decimals`                             | Token decimals (TRX: 6)                                                                                |
| `amount`                               | Human-readable amount, string, already scaled by decimals                                              |
| `status`                               | Always `SUCCESS`                                                                                       |
| `event_type`                           | `CONFIRMED` (19 blocks; safe to credit) or `FINALIZED` (38 blocks; final, safe to release withdrawals) |
| `timestamp`                            | On-chain time in milliseconds; not for idempotency                                                     |

### Idempotency

Each transaction produces two notifications - `CONFIRMED` then `FINALIZED` - with the same `uuid`. If both sender and recipient of one transfer are watched, each side additionally gets its own pair: same `unique_id`, but different `wallet_address`, `is_send`, and `uuid`.

Use `uuid` + `event_type` for idempotency - `uuid` alone would drop the `FINALIZED` stage, and `unique_id` alone would drop one side of a watched-to-watched transfer.

After verifying and processing a webhook, return HTTP 200 (exactly; the response body is ignored). Any other status or a failed request is retried with exponential backoff, indefinitely - so your endpoint must be idempotent.

### Recommended Handling Steps

1. Read the raw request body and verify the signature with the public key.
2. Parse the JSON and check that `status` is `SUCCESS`.
3. Deduplicate by `uuid` + `event_type`.
4. Optionally re-verify the transaction hash, direction, token, and amount via TRON RPC.
5. Apply your business logic and return HTTP 200.
