# Webhook System — Signal Forwarder

This document describes the two outbound channels the forwarder uses to notify downstream systems when a Telegram signal arrives.

| Channel | Transport | Trigger | Payload |
|---------|-----------|---------|---------|
| **Verbatim Signal Webhook** | `POST` to `SIGNAL_WEBHOOK_URL` | **Every** non-promo message | Raw message text + metadata (JSON) |
| **Price-Augmented Bot Message** | Telegram Bot API `sendMessage` | Only **entry** signals on `price_augment` rules | HTML with live bid/ask/spread |

Both channels fire independently. Use the **webhook** for automation pipelines; use the **bot message** for human monitoring in a Telegram admin chat.

---

## 1. Verbatim Signal Webhook (HTTP POST)

### 1.1 Purpose

Send the **complete, unmodified message history** from a source channel to an external HTTP endpoint. This lets downstream systems react to **every** signal type — entries, updates, closes, partials, results — without needing Telegram client access.

> The destination Telegram channel still receives a verbatim copy of the message. The webhook is an **additional** copy sent to your automation server.

### 1.2 Trigger Conditions

The webhook fires for **every** message that passes the pair-level filters (`_should_forward`) **except** promos:

```
Telegram NewMessage
  → matches a source_chat_id in enabled pairs
  → is NOT classified as promo
  → webhook POST fires BEFORE forwarding to destination
```

This means your downstream system receives:
- `ENTRY` signals
- `ENTRY_PENDING` (limit/stop) signals
- `MANAGE` signals (move SL, partial close, breakeven)
- `RESULT` signals (TP hit, SL hit, profit report)
- `UNKNOWN` / `NOISE` (any non-promo text)

### 1.3 Environment Configuration

Add to the forwarder's `.env`:

```bash
# Required — where to POST every signal
SIGNAL_WEBHOOK_URL=https://your-server.example.com/webhook/signals

# Optional — secret used to sign the payload (HMAC-SHA256)
SIGNAL_WEBHOOK_SECRET=change-me-to-a-random-32-char-string
```

> If `SIGNAL_WEBHOOK_URL` is empty or missing, the webhook step is silently skipped.

### 1.4 Request Format

**Method:** `POST`  
**Content-Type:** `application/json`  
**User-Agent:** `ssfx-signal-forwarder/1.0`  
**Timeout:** 15 seconds (fire-and-forget; failure does not block forwarding)

**Payload:**

```json
{
  "event": "signal",
  "received_at_ms": 1752184567890,
  "snapshot": {
    "source_chat_id": -1001234567890,
    "source_message_id": 45678,
    "signal_text": "XAUUSD BUY @ 4120.50\nSL: 4115.00\nTP: 4130.00",
    "signal_date": 1752184567,
    "reply_to_message_id": 45670
  }
}
```

**Field Reference:**

| Field | Type | Description |
|-------|------|-------------|
| `event` | string | Always `"signal"` |
| `received_at_ms` | int | Unix epoch milliseconds when the forwarder received the message |
| `snapshot.source_chat_id` | int | Telegram channel ID where the message originated |
| `snapshot.source_message_id` | int | Telegram message ID within that channel |
| `snapshot.signal_text` | string | Raw message text (verbatim, unmodified) |
| `snapshot.signal_date` | int | Message timestamp (Unix epoch seconds) or `null` |
| `snapshot.reply_to_message_id` | int | If this message is a reply, the parent message ID; else `null` |

### 1.5 HMAC Signature (Optional)

If `SIGNAL_WEBHOOK_SECRET` is configured, the forwarder signs every payload:

```
X-Signal-Signature: sha256=<hex_hmac>
```

The HMAC is computed as:

```python
digest = hmac.new(
    key=SIGNAL_WEBHOOK_SECRET.encode("utf-8"),
    msg=json_payload_bytes,
    digestmod=hashlib.sha256,
).hexdigest()
```

Your receiver **should verify this signature** to ensure the payload came from your forwarder and was not tampered with in transit.

### 1.6 Receiver Examples

#### Python (Flask)

```python
import hmac, hashlib, json
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = b"change-me-to-a-random-32-char-string"

@app.route("/webhook/signals", methods=["POST"])
def receive_signal():
    payload_bytes = request.get_data()
    sig_header = request.headers.get("X-Signal-Signature", "")

    # Verify HMAC
    expected = hmac.new(WEBHOOK_SECRET, payload_bytes, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(f"sha256={expected}", sig_header):
        return jsonify({"error": "invalid signature"}), 401

    data = request.get_json()
    snapshot = data["snapshot"]

    print(f"Signal #{snapshot['source_message_id']}: {snapshot['signal_text'][:80]}")
    # → Route to your trading engine, database, alert system, etc.

    return jsonify({"status": "ok"}), 200
```

#### Node.js (Express)

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = 'change-me-to-a-random-32-char-string';

app.post('/webhook/signals', (req, res) => {
    const sig = req.headers['x-signal-signature'] || '';
    const expected = 'sha256=' + crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
        return res.status(401).json({ error: 'invalid signature' });
    }

    const data = JSON.parse(req.body);
    console.log(`Signal #${data.snapshot.source_message_id}:`, data.snapshot.signal_text);

    // Your logic here: persist, route, trigger trades, etc.
    res.json({ status: 'ok' });
});
```

#### Go

```go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
)

const webhookSecret = "change-me-to-a-random-32-char-string"

type SignalPayload struct {
    Event      string `json:"event"`
    ReceivedAt int64  `json:"received_at_ms"`
    Snapshot   struct {
        SourceChatID      int64  `json:"source_chat_id"`
        SourceMessageID   int    `json:"source_message_id"`
        SignalText        string `json:"signal_text"`
        SignalDate        *int64 `json:"signal_date"`
        ReplyToMessageID  *int64 `json:"reply_to_message_id"`
    } `json:"snapshot"`
}

func signalsWebhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-Signal-Signature")

    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(sig), []byte(expected)) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    var payload SignalPayload
    json.Unmarshal(body, &payload)

    // Process signal...
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```

---

## 2. Price-Augmented Bot Message (Telegram)

### 2.1 Purpose

When an **entry signal** arrives on a pair with `price_augment = true`, the forwarder reads the live cTrader bid/ask from its in-memory cache and sends a rich HTML message to a Telegram admin chat. This is for **human monitoring** — verifying entry prices, spreads, and latency at signal time.

> This is **not** a generic webhook. It is a Telegram Bot API message sent to `SIGNAL_ADMIN_CHAT_ID`.

### 2.2 Trigger Conditions

```
Telegram NewMessage
  → matches a source_chat_id in enabled pairs
  → pair.price_augment == true
  → signal is classified as high-confidence ENTRY
  → cTrader spot client is connected
  → read cached tick for augment_symbol (or CTRADER_SYMBOL)
  → send HTML snapshot to SIGNAL_ADMIN_CHAT_ID via Bot API
```

### 2.3 Environment Configuration

```bash
# Required — the bot that sends admin messages
SIGNAL_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxyz

# Required — Telegram chat ID of the admin / monitoring channel
SIGNAL_ADMIN_CHAT_ID=-1009876543210

# Optional — symbol watched if pair has no override
CTRADER_SYMBOL=XAUUSD
CTRADER_SYMBOL_ID=41
CTRADER_PRICE_TIMEOUT=8.0
```

### 2.4 Message Format

The bot sends an HTML-formatted message:

```html
<b>Price-augmented signal</b> (#45678)

XAUUSD BUY @ 4120.50
SL: 4115.00
TP: 4130.00

<b>XAUUSD</b> at signal time:
  Bid: <code>4120.45</code>
  Ask: <code>4120.62</code>
  Spread: <code>0.17</code>
  Latency: 23 ms
  cTrader tick ts: 2026-07-10 14:23:45 (1752184625000)

<code>received=1752184567890 | fetched=1752184567913</code>
```

### 2.5 Persisted Snapshot

The same data is also stored in the database (`signal_log` table):

```sql
SELECT * FROM signal_log
WHERE source_chat_id = -1001234567890
ORDER BY id DESC LIMIT 5;
```

| Column | Example |
|--------|---------|
| `source_chat_id` | -1001234567890 |
| `source_message_id` | 45678 |
| `signal_text` | "XAUUSD BUY @ 4120.50..." |
| `symbol` | XAUUSD |
| `bid` | 4120.45000 |
| `ask` | 4120.62000 |
| `spread` | 0.17000 |
| `price_available` | 1 |
| `received_at_ms` | 1752184567890 |
| `price_fetched_at_ms` | 1752184567913 |
| `tick_timestamp_ms` | 1752184625000 |

Retrieve via API:

```bash
curl -H 'Cookie: session=...' \
  'https://quill.nx.kg/f/api/signals?source_chat_id=-1001234567890'
```

---

## 3. Comparison Table

| | Verbatim Webhook | Price-Augmented Bot Message |
|---|---|---|
| **Transport** | HTTP POST | Telegram Bot API `sendMessage` |
| **Trigger** | Every non-promo message | Only entry signals on `price_augment` rules |
| **Payload** | Raw JSON with full message text | HTML with live bid/ask/spread |
| **Latency** | ~ms (fire-and-forget) | ~ms (cached tick read) |
| **Authentication** | HMAC-SHA256 signature | Implicit (bot token) |
| **Use case** | Automation pipelines, trading bots, databases | Human monitoring, price verification |
| **Storage** | Not stored by forwarder (your receiver stores it) | Stored in `signal_log` + visible in dashboard |
| **Failure handling** | Logged warning; never blocks forwarding | Logged warning; never blocks forwarding |

---

## 4. Complete Signal Processing Flow

```
Telegram source channel receives a message
  │
  ▼
┌────────────────────────────────────────┐
│  SignalParser.classify(text)           │
│  → ENTRY / ENTRY_PENDING / MANAGE /    │
│     RESULT / NOISE / PROMO / UNKNOWN   │
└────────────────────────────────────────┘
  │
  ├─► [Webhook] send_signal_webhook()
  │     POST to SIGNAL_WEBHOOK_URL
  │     (every signal except promo)
  │
  ├─► [Destination] forward_message()
  │     Copy verbatim to destination channel
  │     (every signal that passes filters)
  │
  └─► [Price Augment] if ENTRY + price_augment
        send_augmented_snapshot()
        ├── read cached tick from CTraderSpotClient
        ├── store in signal_log (MariaDB)
        ├── send HTML to SIGNAL_ADMIN_CHAT_ID (Bot API)
        └── POST to SIGNAL_WEBHOOK_URL (same webhook as above,
            but the webhook receives the RAW text, not the price)
```

---

## 5. Configuration Recipes

### 5.1 Minimal Webhook-Only Setup

Receive raw signals in your automation server. No Telegram bot needed.

```bash
SIGNAL_WEBHOOK_URL=https://api.yourtradingbot.com/v1/signals
SIGNAL_WEBHOOK_SECRET=sk_live_abc123xyz789
# SIGNAL_BOT_TOKEN=          # leave empty
# SIGNAL_ADMIN_CHAT_ID=      # leave empty
```

### 5.2 Minimal Bot-Only Setup

Receive price-augmented snapshots in a Telegram admin chat. No HTTP endpoint needed.

```bash
SIGNAL_BOT_TOKEN=123456789:ABCdef...
SIGNAL_ADMIN_CHAT_ID=-1009876543210
# SIGNAL_WEBHOOK_URL=        # leave empty
# SIGNAL_WEBHOOK_SECRET=     # leave empty
```

### 5.3 Full Setup (Recommended)

Automation pipeline + human monitoring + cTrader prices.

```bash
SIGNAL_BOT_TOKEN=123456789:ABCdef...
SIGNAL_ADMIN_CHAT_ID=-1009876543210
SIGNAL_WEBHOOK_URL=https://api.yourtradingbot.com/v1/signals
SIGNAL_WEBHOOK_SECRET=sk_live_abc123xyz789

CTRADER_CLIENT_ID=your_ct_client_id
CTRADER_CLIENT_SECRET=your_ct_client_secret
CTRADER_ACCOUNT_ID=46659639
CTRADER_USE_LIVE=false
CTRADER_SYMBOL=XAUUSD
CTRADER_SYMBOL_ID=41
CTRADER_PRICE_TIMEOUT=8.0
```

---

## 6. Downstream System Integration Patterns

### 6.1 Pattern A: Webhook → Queue → Worker

Best for high-volume or unreliable networks.

```
Forwarder POST → Your API Gateway → Message Queue (Redis/RabbitMQ/SQS)
                                            ↓
                                    Worker processes signals
                                    (parses, validates, executes)
```

### 6.2 Pattern B: Webhook → Direct Execution

Best for low-latency requirements (sub-100ms end-to-end).

```
Forwarder POST → Your Edge Function (Cloudflare Workers / Vercel)
                       ↓
               Immediate trade decision
               → cTrader Open API order
```

### 6.3 Pattern C: Webhook + Bot Hybrid

Best for copy-trading with oversight.

```
Forwarder ──► Webhook ──► Your risk engine ──► cTrader order
       │
       └──► Bot message ──► Admin chat (human reviews price/slippage)
```

---

## 7. Security Checklist

- [ ] `SIGNAL_WEBHOOK_SECRET` is ≥ 32 random characters
- [ ] Receiver verifies `X-Signal-Signature` before processing
- [ ] `SIGNAL_WEBHOOK_URL` uses HTTPS (never HTTP)
- [ ] Receiver endpoint is idempotent (forwarder may retry on timeout)
- [ ] `SIGNAL_BOT_TOKEN` is not committed to Git
- [ ] Admin chat (`SIGNAL_ADMIN_CHAT_ID`) is private / restricted
- [ ] Webhook receiver rate-limits by IP or signature to prevent DoS

---

## 8. Troubleshooting

| Problem | Likely Cause | Fix |
|---------|-------------|-----|
| No webhook received | `SIGNAL_WEBHOOK_URL` not set | Add to `.env` and restart |
| Webhook fires but payload rejected | Receiver missing signature check | Implement HMAC verification |
| Bot messages not sent | `SIGNAL_BOT_TOKEN` missing or invalid | Verify token with Telegram `@BotFather` |
| Price data missing in bot message | cTrader spot client not started | Check `CTRADER_ACCOUNT_ID` and that a `price_augment` pair exists |
| `signal_log` empty | No `price_augment` rules enabled | Enable **Price augment** checkbox on the pair |
| Duplicate webhooks for same message | Forwarder retried after timeout | Make receiver idempotent (dedupe by `source_message_id`) |

---

## 9. Related Documentation

- `README.md` — Project overview and quick start
- `DEPLOY.md` — cPanel deployment steps
- `INTEGRATION.md` — How to decrypt cTrader tokens for trading API access

---

*Project: `/Volumes/ExMac/code/ssfx/alwaydata-v3` — deployed on cPanel shared hosting (`quill.nx.kg/f`)*
*Webhook version: 1.0 — ssfx-signal-forwarder/1.0*
