The first version of this I built was a single Flask endpoint that took a POST from TradingView, read the JSON, and fired a market order. It worked on the first try, which should have been a warning. Anything that maps an open URL directly to a live order is one curl command away from someone else trading your account, and TradingView alerts carry no signature by default, so that URL really is open. The whole job of the relay in the middle is to close that gap without giving up the thing that makes this worth doing, which is that your exchange keys never leave your own machine and no third party can move your money.
So the shape I keep coming back to is three moving parts. TradingView fires a webhook. A small relay you control receives it, validates it, and turns the signal into a sized order. The relay talks to the exchange over a trade-only API key. Nothing in that chain is exotic. The interesting decisions all sit in the validation and the sizing, the two places a lazy version quietly loses money or gets abused.
Why the naive receiver gets owned
Start with the two attacks that hit a plain receiver, because understanding these makes you build the rest correctly by default.
The first is spoofing. Your webhook URL is a secret only in the sense that it is long, and webhook URLs leak constantly through logs, browser history, screen shares, and pasted config. Once it leaks, anyone can POST a fake buy signal. The fix people reach for first is TradingView's own message field. You put a shared secret string inside the alert JSON, and the relay checks it. That is better than nothing, and it is where a lot of hobby setups stop. The weakness is that the secret sits in plaintext in the body, so anyone who ever sees one real payload, a proxy log included, has it forever. To be more serious, front the relay with something that verifies an HMAC signature, or restrict inbound connections to TradingView's published webhook IP ranges so a random attacker cannot reach the endpoint. I like combining the IP allowlist with a body secret, because the two failure modes are different and it is unlikely both leak at once.
The second attack is replay, and this one is nastier because it needs no secret at all. An attacker who captures one legitimate payload, secret included, can just send it again, and again, and your relay sees a valid buy signal every time and dutifully opens a position on each one. Even without an attacker, replay bites by accident, because webhook senders retry on timeout and a slow response turns one alert into three identical orders. The defense is to make every payload single-use. TradingView can inject a timestamp and a unique-ish value into the alert, and your relay rejects anything older than a small window, and any ID it has seen before. That combination, a freshness window plus a used-ID set, is what actually stops replay. A secret alone does not.
The validation gate, in order
Here is the checklist every inbound webhook runs before it is allowed to become an order. Order matters, cheapest checks first, so you reject junk before spending any real work on it.
- Reject anything that is not a POST with the content type you expect. This alone kills most drive-by scanning traffic.
- Confirm the source IP is in the allowlist, if you are using one. No point parsing a body from an address that cannot be legitimate.
- Parse the JSON in a way that fails closed. A malformed body is a reject, never a default.
- Check the shared secret with a constant-time comparison, so you do not leak information through how long the check takes.
- Check freshness. If the embedded timestamp is older than roughly a minute or two, drop it.
- Check the unique ID against your recently-seen set. Seen it before, drop it.
- Only now, validate the trading fields. Is the symbol one you trade, is the side buy or sell, is the size within your bounds.
That last point is where a surprising number of accidents live. If your relay trusts a position size that arrived in the payload, a spoofed or fat-fingered alert can ask for a size you never intended. I do not let the webhook dictate absolute size at all. The alert says direction and maybe a strength bucket. The relay decides the actual quantity from account equity and a risk cap that lives in the relay config, not in the message. The webhook says which way, and the relay decides how much.
Trade-only keys, and what that word actually buys you
The reason this pattern beats handing your account to a hosted copy-trade service is custody. Most exchanges let you scope an API key. The key on your relay should carry trade permission and nothing else. Withdrawal permission stays off. Where the exchange supports it, bind the key to your relay's IP so a stolen key is useless from anywhere else.
Be honest about what that scoping does and does not protect you from. A trade-only key cannot pull your funds off the exchange, which is the failure that actually wipes people out, so that is the big win. It does not stop a compromised relay from opening bad trades with the funds that are there. Someone who owns your relay can still churn your account into fees or open a position at the worst possible moment. So trade-only is the floor, not the ceiling. You still want the relay locked down, the key stored somewhere better than a plaintext file next to the code, and a hard ceiling on order size and open positions enforced in the relay so even a fully compromised signal path cannot bet the whole account on one order.
One more failure mode worth naming, because it is boring and it is the one that actually happens. Duplicate fills from your own retries. If the relay times out talking to the exchange and retries, you place the same order twice. The fix is an idempotency key derived from the alert's unique ID, passed to the exchange as a client order ID. If the exchange has already seen that client order ID, it rejects the duplicate instead of filling it. You get this almost for free once you already have per-alert unique IDs for replay defense, one piece of plumbing solving two problems.
Before you point it at real money
Run the whole path against a testnet or a tiny live balance first, and try to break your own receiver on purpose. Replay a captured payload and confirm the second one is rejected. Send a symbol you do not trade and confirm it is dropped. Send an absurd size and confirm the relay clamps it to your cap rather than obeying it. Kill the exchange connection mid-order and confirm your idempotency key stops a double fill on retry. If all four behave, you have covered the failure modes that hurt most.
The signal side is its own separate problem, and honestly the harder one. A clean relay will faithfully execute a bad strategy forever. When I am sanity-checking whether an alert is worth wiring to live orders at all, I lean on backtests and the market scorecards we keep in Blockcircle before any key touches it, because the plumbing being correct tells you nothing about whether the trade is. Get the receiver boring and predictable, keep the keys trade-only, and spend your real attention on whether the signal deserves to fire.