<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HookWatch</title>
    <description>The latest articles on DEV Community by HookWatch (@hookwatch).</description>
    <link>https://dev.to/hookwatch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179654%2F054f1a2b-2713-4f8b-93c4-920d5d2d2bf5.png</url>
      <title>DEV Community: HookWatch</title>
      <link>https://dev.to/hookwatch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hookwatch"/>
    <language>en</language>
    <item>
      <title>Connecting an AI trading agent to TradingView alerts without losing signals</title>
      <dc:creator>HookWatch</dc:creator>
      <pubDate>Mon, 12 Oct 2026 23:32:40 +0000</pubDate>
      <link>https://dev.to/hookwatch/connecting-an-ai-trading-agent-to-tradingview-alerts-without-losing-signals-1b1p</link>
      <guid>https://dev.to/hookwatch/connecting-an-ai-trading-agent-to-tradingview-alerts-without-losing-signals-1b1p</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://gethookwatch.com" rel="noopener noreferrer"&gt;HookWatch&lt;/a&gt;, a hosted TradingView webhook reliability layer. Everything below works without it, and the code is MIT on GitHub.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A lot of people are wiring LLM agents to TradingView right now: an alert fires, the agent looks at the signal plus context (position, news, PnL), decides, and places an order. The first version usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 3-15 seconds
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works in testing, then you start losing signals in production. Here's why, and the boring pattern that fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TradingView gives you about 3 seconds
&lt;/h2&gt;

&lt;p&gt;From TradingView's own docs: if the receiving server takes longer than about 3 seconds, the request is cancelled. Also: ports 80/443 only, no IPv6, no custom headers, and the body is &lt;code&gt;text/plain&lt;/code&gt; unless your alert message is valid JSON.&lt;/p&gt;

&lt;p&gt;An LLM call alone usually takes longer than 3 seconds. So the handler above times out, TradingView treats the delivery as failed, and depending on where the code was when the connection dropped, your agent may or may not have acted. You get no retry you can rely on and no log on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split the handler from the agent
&lt;/h2&gt;

&lt;p&gt;The webhook handler should do four cheap things and return:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify a secret&lt;/strong&gt; carried in the JSON body (TradingView can't send auth headers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-duplicate&lt;/strong&gt; on an &lt;code&gt;alert_id&lt;/code&gt; you build in the alert message, e.g. &lt;code&gt;{{ticker}}-{{interval}}-{{timenow}}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp and enqueue&lt;/strong&gt; the signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return 200&lt;/strong&gt; within milliseconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent runs in a separate consumer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;alert_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;alert_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;alert_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# duplicates get 200 too
&lt;/span&gt;    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_received_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_nowait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueFull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;busy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;agent_decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# slow is fine here
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;risk_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full runnable versions (FastAPI, Express, and the agent one): &lt;a href="https://github.com/hookwatch-ops/tradingview-webhook-examples" rel="noopener noreferrer"&gt;github.com/hookwatch-ops/tradingview-webhook-examples&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a deterministic risk gate after the agent
&lt;/h2&gt;

&lt;p&gt;The agent proposes; plain code decides whether anything reaches the broker. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Max signal age.&lt;/strong&gt; If the queue backed up and a signal is 2 minutes old, a market order on it is probably wrong. Drop it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position and size limits&lt;/strong&gt; the model can't talk its way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output only.&lt;/strong&gt; Parse the agent's answer into a fixed schema; anything else is a skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part a fast handler doesn't fix: downtime
&lt;/h2&gt;

&lt;p&gt;Fast ACK solves the 3-second problem. It doesn't help if your process is restarting, your VPS reboots, or you're deploying a new agent prompt at the moment the alert fires. TradingView doesn't queue alerts for you.&lt;/p&gt;

&lt;p&gt;The fix is to make ingress separate from the bot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a tiny always-on receiver (or serverless function) that only writes to a durable queue: Redis Streams, SQS, Postgres&lt;/li&gt;
&lt;li&gt;durable dedupe: Redis &lt;code&gt;SET alert_id 1 NX EX 86400&lt;/code&gt; or a unique index&lt;/li&gt;
&lt;li&gt;retries with backoff from the queue to the agent, and a &lt;strong&gt;dead-letter queue&lt;/strong&gt; for signals that keep failing, so you can see what was missed&lt;/li&gt;
&lt;li&gt;an alert to yourself when no signals arrive during hours you expect them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then your agent can crash, restart and redeploy without signals vanishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you don't want to run that part
&lt;/h2&gt;

&lt;p&gt;That ingress layer is what HookWatch does as a service: you point the TradingView alert at your HookWatch URL, it answers TradingView right away, queues the alert, delivers it to your receiver, retries failed deliveries (5 automatic retries over about 2 minutes) and keeps anything that still fails in a dead-letter queue. It's not signals and not a broker, and it doesn't make alerts faster; it just stops them from disappearing silently. There's a free trial (300 alerts or 7 days, no card) if you want to try it in front of your agent.&lt;/p&gt;

&lt;p&gt;Either way, keep the secret check and the idempotency in your own receiver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Handler returns 200 in well under 3 s, never calls the LLM&lt;/li&gt;
&lt;li&gt;[ ] Secret in the JSON body, constant-time compare&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;alert_id&lt;/code&gt; in every alert message, de-duplicated durably&lt;/li&gt;
&lt;li&gt;[ ] Agent runs from a queue, with a max-signal-age check&lt;/li&gt;
&lt;li&gt;[ ] Deterministic risk gate between agent and broker&lt;/li&gt;
&lt;li&gt;[ ] Ingress survives bot restarts (your own queue or a hosted layer)&lt;/li&gt;
&lt;li&gt;[ ] You look at failures (DLQ / logs), not just successes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Not financial advice. Paper-trade any agent before giving it real money.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tradingview</category>
      <category>webhooks</category>
      <category>ai</category>
      <category>trading</category>
    </item>
    <item>
      <title>TradingView webhooks and the 3-second timeout: ACK fast, deliver later</title>
      <dc:creator>HookWatch</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:09:01 +0000</pubDate>
      <link>https://dev.to/hookwatch/tradingview-webhooks-and-the-3-second-timeout-ack-fast-deliver-later-1pa8</link>
      <guid>https://dev.to/hookwatch/tradingview-webhooks-and-the-3-second-timeout-ack-fast-deliver-later-1pa8</guid>
      <description>&lt;p&gt;TradingView sends alert webhooks as HTTP POST requests to a URL you choose, with the alert message as the body. Its docs are short and strict: "If a remote server takes longer than three seconds to process a request, the request will be cancelled" (&lt;a href="https://www.tradingview.com/support/solutions/43000529348-how-to-configure-webhook-alerts/" rel="noopener noreferrer"&gt;How to configure webhook alerts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;TradingView resends in one case only: when your server answers with a status between 500 and 599, &lt;strong&gt;except 504&lt;/strong&gt;. It waits 5 seconds and resends, up to 3 times, so at most 4 sends per trigger (&lt;a href="https://www.tradingview.com/support/solutions/43000735201-webhook-resubmission/" rel="noopener noreferrer"&gt;Webhook resubmission&lt;/a&gt;). It doesn't resend timeouts.&lt;/p&gt;

&lt;p&gt;A few other limits from the same docs are worth knowing before you debug anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only ports 80 and 443 are accepted.&lt;/li&gt;
&lt;li&gt;IPv6 isn't supported, so your hostname needs an IPv4 (A) record.&lt;/li&gt;
&lt;li&gt;Webhook alerts only work with two-factor authentication enabled on the TradingView account.&lt;/li&gt;
&lt;li&gt;The request is sent as &lt;code&gt;application/json&lt;/code&gt; if the message is valid JSON, otherwise as &lt;code&gt;text/plain&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What goes wrong in practice
&lt;/h2&gt;

&lt;p&gt;If your endpoint does real work inside the request (calling a broker API, writing to a sheet, posting to Discord), you're racing that clock. People often report the same pattern: the alert log flags the webhook as failed, yet the destination got the data. The work happened, but the 200 came back too late. The &lt;strong&gt;Webhook status&lt;/strong&gt; column in TradingView's alert log is where to look.&lt;/p&gt;

&lt;p&gt;Restarts are subtler. If a reverse proxy answers 502 or 503 while your app restarts, TradingView will resend (three times, 5 s apart), and you may get the alert once the app is back. But if the connection is refused, the request times out, or the proxy answers 504, there's no resend and that alert is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that fixes it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Receive the POST and check a shared secret. TradingView's alert setup gives you a URL and a message, so put the secret in the message body (or in an unguessable URL path), not in a custom header.&lt;/li&gt;
&lt;li&gt;Write the payload to a durable queue and return &lt;strong&gt;200 immediately&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Have a worker deliver it to your bot, with its own retries and backoff.&lt;/li&gt;
&lt;li&gt;Make handling idempotent, because the same alert can arrive more than once.&lt;/li&gt;
&lt;li&gt;Park messages that keep failing in a dead-letter queue (DLQ), so you can inspect them instead of losing them.&lt;/li&gt;
&lt;li&gt;Reconcile TradingView's alert log against your order log daily, and keep protective stops resting at the broker.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Python (Flask sketch)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-shared-secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Persist the raw alert (Redis list, SQLite/Postgres table...). Must be fast and durable.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# persist first
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# ACK before any broker / Redis / Discord work
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node (Express sketch)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// persist first (your durable queue)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ACK before slow work&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a message like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-shared-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ticker}}-{{interval}}-{{time}}-{{strategy.order.action}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{strategy.order.action}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ticker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ticker}}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; gives you idempotency. Use the bar time &lt;code&gt;{{time}}&lt;/code&gt;, not &lt;code&gt;{{timenow}}&lt;/code&gt;, so the id is tied to the bar that fired and stays the same across resends. Two caveats: &lt;code&gt;{{strategy.order.action}}&lt;/code&gt; only exists in strategy alerts, and messages built with Pine's &lt;code&gt;alert()&lt;/code&gt; function don't substitute placeholders at all (&lt;a href="https://www.tradingview.com/pine-script-docs/faq/alerts/" rel="noopener noreferrer"&gt;Pine FAQ: alerts&lt;/a&gt;). For those, build the same string in Pine with &lt;code&gt;str.format()&lt;/code&gt; from &lt;code&gt;syminfo.ticker&lt;/code&gt;, &lt;code&gt;timeframe.period&lt;/code&gt; and &lt;code&gt;time&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;We build HookWatch, which runs this ACK, queue, retry and DLQ layer as a hosted service for TradingView webhooks. Each alert is queued and answered with a 200 well inside TradingView's window. If your bot doesn't accept it, we retry delivery 5 times (after 2, 5, 15, 30 and 60 seconds, about 2 minutes in total). Alerts that still fail are parked in a dead-letter queue instead of being lost, and support can resend them. There's also an optional heartbeat that emails you if TradingView stops sending. It isn't a signal service and isn't a broker, just delivery. Keep your bridge; we handle the delivery hop to it. You still want the idempotency key above, because any retry layer, ours included, can deliver an alert more than once.&lt;/p&gt;

&lt;p&gt;Starter is €19/month or €190/year (up to 3 destinations); Ops is €49/month or €490/year (up to 15). There's a free trial: 300 alerts or 7 days, whichever comes first, no card.&lt;/p&gt;

&lt;p&gt;If you've built your own receiver for TradingView alerts, we'd like to hear how you handle retries and duplicates, and what you'd need to see before trusting a hosted hop in front of your bot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gethookwatch.com" rel="noopener noreferrer"&gt;https://gethookwatch.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: We build HookWatch, the service described above. This post was drafted with AI assistance and reviewed and edited by the HookWatch team.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>showdev</category>
      <category>tradingview</category>
      <category>webhooks</category>
    </item>
  </channel>
</rss>
