<?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: Andrey Schurko</title>
    <description>The latest articles on DEV Community by Andrey Schurko (@andreyschurko).</description>
    <link>https://dev.to/andreyschurko</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%2F4154017%2Fc61e957d-1993-41bb-9306-2f607b70e187.jpeg</url>
      <title>DEV Community: Andrey Schurko</title>
      <link>https://dev.to/andreyschurko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andreyschurko"/>
    <language>en</language>
    <item>
      <title>Hearing every Polymarket trade three ways: the detection layer of a copy-trading bot in Rust</title>
      <dc:creator>Andrey Schurko</dc:creator>
      <pubDate>Thu, 01 Oct 2026 07:47:30 +0000</pubDate>
      <link>https://dev.to/andreyschurko/hearing-every-polymarket-trade-three-ways-the-detection-layer-of-a-copy-trading-bot-in-rust-15a</link>
      <guid>https://dev.to/andreyschurko/hearing-every-polymarket-trade-three-ways-the-detection-layer-of-a-copy-trading-bot-in-rust-15a</guid>
      <description>&lt;p&gt;A copy-trading bot has one job that matters more than any other: &lt;strong&gt;notice that the wallet you follow just traded.&lt;/strong&gt; Everything after that, from sizing and slippage control to execution and settlement, works on a signal the detection layer either delivered or didn't.&lt;/p&gt;

&lt;p&gt;On 31 August 2026 Polymarket's &lt;code&gt;activity/trades&lt;/code&gt; websocket topic went down platform-wide, for every IP at once, for hours. Every bot that listened only to that topic went deaf. Mine had a slow REST poll as a backup, and that was all it had.&lt;/p&gt;

&lt;p&gt;That day is why &lt;a href="https://github.com/AndreySchurko/garnet-polymarket" rel="noopener noreferrer"&gt;Garnet&lt;/a&gt;, the self-hosted Polymarket copy-trading engine I maintain, now hears every trade &lt;strong&gt;three independent ways&lt;/strong&gt;. This post covers how the three circuits work, why they write into one table, and the measurements behind each design decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three circuits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Depends on&lt;/th&gt;
&lt;th&gt;Silence means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTDS websocket&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fastest&lt;/td&gt;
&lt;td&gt;Polymarket's websocket&lt;/td&gt;
&lt;td&gt;a dead subscription (watchdog: 45 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;/activity&lt;/code&gt; poll&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;slow (every 3 s)&lt;/td&gt;
&lt;td&gt;Polymarket's REST API&lt;/td&gt;
&lt;td&gt;nothing — it is the safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Polygon logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not faster than RTDS&lt;/td&gt;
&lt;td&gt;your own node&lt;/td&gt;
&lt;td&gt;the leaders were not trading (watchdog: 5 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key idea fits on one line: &lt;strong&gt;a third delivery of one truth, not a third truth.&lt;/strong&gt; All three circuits write to the same table and collapse onto the same dedup key. Which copy arrives first doesn't matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit 1: the RTDS websocket, and the keepalive that cost 2.5×
&lt;/h2&gt;

&lt;p&gt;Polymarket's real-time data socket (RTDS) is the fastest source. You subscribe to the activity topic and receive trade frames as they happen.&lt;/p&gt;

&lt;p&gt;The surprising lesson came from a sensible-looking habit. Most websocket clients send a periodic keepalive. When I measured delivery with and without one, &lt;strong&gt;a keepalive cut delivery by 2.5×&lt;/strong&gt;. Since then, Garnet writes exactly two things to that socket: the subscription frame, and a &lt;code&gt;Pong&lt;/code&gt; when the server asks for one. Nothing else.&lt;/p&gt;

&lt;p&gt;The second lesson was about silence. A socket can be connected and healthy while delivering nothing, because the subscription quietly died. "The process is alive" says nothing about whether trades are flowing. So the RTDS circuit has a &lt;strong&gt;45-second silence watchdog&lt;/strong&gt;: if the topic goes quiet for that long, the subscription is treated as dead and rebuilt.&lt;/p&gt;

&lt;p&gt;This generalises into a rule the whole bot follows: &lt;strong&gt;health is measured by flow, not by liveness.&lt;/strong&gt; Two process-level guards once reported OK while the bot had been blind for 6.5 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit 2: &lt;code&gt;/activity&lt;/code&gt; polling, and why history is not a signal
&lt;/h2&gt;

&lt;p&gt;The safety net is boring on purpose: every 3 seconds, ask the REST API what each followed wallet did recently. It is slow, but it depends on nothing except an HTTP endpoint answering.&lt;/p&gt;

&lt;p&gt;It also contains the nastiest trap in the whole system. &lt;code&gt;/activity?user=…&amp;amp;limit=20&lt;/code&gt; on a quiet wallet returns &lt;strong&gt;weeks&lt;/strong&gt; of history. On the first tick after you add a wallet, all of that looks like news. The first version of this logic produced 69 "market not tradable" refusals and &lt;strong&gt;28 copies of trades up to 3.4 days old&lt;/strong&gt;. One of them filled at 0.001 against the leader's 0.260.&lt;/p&gt;

&lt;p&gt;The fix is a rule: &lt;strong&gt;a wallet is copied from the moment it was assigned, never retroactively.&lt;/strong&gt; Every trade older than &lt;code&gt;wallets.created_at&lt;/code&gt; is still &lt;em&gt;recorded&lt;/em&gt; (otherwise the dedup would forget it and the next tick would bring the same history back), but it never becomes a signal. The comparison happens at one-second resolution, because RTDS timestamps are in seconds while &lt;code&gt;created_at&lt;/code&gt; has microseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit 3: Polygon logs, decoded independently
&lt;/h2&gt;

&lt;p&gt;The third circuit doesn't touch Polymarket's infrastructure at all. Every fill on Polymarket's exchange ends up as an &lt;code&gt;OrderFilled&lt;/code&gt; event on Polygon, and a node will push those events to you over &lt;code&gt;eth_subscribe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three details made this circuit work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Decode from the V2 ABI, not from memory.&lt;/strong&gt; The V1 event (five data words, side inferred from which asset ID is zero) yields &lt;strong&gt;not one&lt;/strong&gt; log on the V2 exchange, while that exchange emits 45,911 fills over 500 blocks. In V2 the event has seven data words, and the side is an explicit &lt;code&gt;side: uint8&lt;/code&gt; field. The decoder was written from the verified V2 ABI rather than carried over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Filter on the node, by maker.&lt;/strong&gt; Without a wallet filter you receive the platform's entire feed: 7,255 fills over 120 blocks, roughly thirty a second. The filter goes on &lt;code&gt;topics[2]&lt;/code&gt;, the maker. You don't need a second filter on the taker, because the aggressor gets an &lt;code&gt;OrderFilled&lt;/code&gt; of its own in which it &lt;em&gt;is&lt;/em&gt; the maker. Of 1,877 addresses seen in &lt;code&gt;topics[3]&lt;/code&gt;, 1,876 also appeared in &lt;code&gt;topics[2]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// The filter is set on the node's side: exchange addresses and topics.&lt;/span&gt;
&lt;span class="cd"&gt;/// The node wakes us when a leader trades, not when anyone at all trades.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;subscribe_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;topic2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;wallet_topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"eth_subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"topics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ORDER_FILLED_TOPIC0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. A log carries no time, and you may not invent one.&lt;/strong&gt; Downstream logic needs the leader's trade time. It decides, for example, when a burst of fills from one order is over. The timestamp has to come from the block (cached). If the node fails to provide it, the circuit does &lt;strong&gt;not&lt;/strong&gt; fall back to the local clock. A trade with an invented time would pass the "assigned after" check by the wrong clock and look real.&lt;/p&gt;

&lt;p&gt;Two more rules: a log removed by a chain reorganisation (&lt;code&gt;removed: true&lt;/code&gt;) is not a trade, because copying it would buy something that no longer exists on chain. And this circuit's silence watchdog is five minutes, not 45 seconds. Here silence usually means the leaders simply weren't trading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it faster?&lt;/strong&gt; No. A log appears once the settlement transaction is in a block, and the CLOB matched the order before that. The chain circuit is more &lt;em&gt;reliable&lt;/em&gt; than the socket, not faster. Nothing about it lets you get ahead of the leader, and I'd be suspicious of any bot that claims otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  One table, one key
&lt;/h2&gt;

&lt;p&gt;All three circuits write into &lt;code&gt;leader_trades&lt;/code&gt;, deduplicated on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(tx_hash, wallet, token_id, side)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions hide in that key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No log index.&lt;/strong&gt; It doesn't exist in the RTDS frame, in &lt;code&gt;/trades&lt;/code&gt;, or in &lt;code&gt;/activity&lt;/code&gt;. Measured over 500 trades, every one had a unique hash, so the hash is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No price or size.&lt;/strong&gt; A redelivery with different rounding would pass as a new trade and &lt;strong&gt;double the stake&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single taker order that sweeps several price levels arrives as several fills with &lt;em&gt;different&lt;/em&gt; transaction hashes. The dedup key can't and shouldn't catch those. That is a separate mechanism, the &lt;em&gt;slice window&lt;/em&gt;, which collapses one leader order into one copy. It exists because on real data the first copy of a wave returned &lt;strong&gt;+7.4%&lt;/strong&gt; while later slices returned &lt;strong&gt;−15%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A losing delivery is still data
&lt;/h2&gt;

&lt;p&gt;The first version did &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;: the second copy of a trade vanished without a trace. That is correct for dedup and fatal for measurement, because it threw away the only evidence of which circuit was actually useful.&lt;/p&gt;

&lt;p&gt;Now the winner goes to &lt;code&gt;leader_trades.source&lt;/code&gt;, and &lt;strong&gt;every&lt;/strong&gt; sighting goes to &lt;code&gt;trade_sightings&lt;/code&gt;, keyed on &lt;code&gt;(leader_trade_id, source)&lt;/code&gt;. A circuit's lag is its &lt;code&gt;ts_seen&lt;/code&gt; minus the earliest sighting of that trade. The Telegram command &lt;code&gt;/sources&lt;/code&gt; shows the picture per circuit, and its main column is "brought by it alone". Zero there means the circuit catches nothing that wouldn't be caught without it. That column is the honest answer to "is this circuit worth running?" One subtlety: zero for &lt;em&gt;every&lt;/em&gt; circuit at once means they duplicate each other, so any one of them can be switched off, but not all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust a single feed.&lt;/strong&gt; The day it goes down is the day you find out it was your only one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't send anything to a socket you only read from.&lt;/strong&gt; Measure before you "keep it alive".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History is not a signal.&lt;/strong&gt; Copy from the moment of assignment, by the leader's clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode from the ABI you verified,&lt;/strong&gt; not from the one you remember.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deduplicate on identity, never on values.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the duplicates as observations.&lt;/strong&gt; You can't defend or switch off a circuit you can't measure.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Garnet is source-available (BUSL-1.1, free for individuals) and self-hosted: your key never leaves your server, and the README lists every host the code talks to. Code, architecture notes with every invariant above, and a shadow mode that trades on paper at real fees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/AndreySchurko/garnet-polymarket" rel="noopener noreferrer"&gt;https://github.com/AndreySchurko/garnet-polymarket&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://andreyschurko.github.io/garnet-polymarket/" rel="noopener noreferrer"&gt;https://andreyschurko.github.io/garnet-polymarket/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about the detection layer are welcome in the comments or in &lt;a href="https://github.com/AndreySchurko/garnet-polymarket/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>polymarket</category>
      <category>trading</category>
      <category>websocket</category>
    </item>
    <item>
      <title>Before you give a Polymarket bot your private key: a 15-minute checklist</title>
      <dc:creator>Andrey Schurko</dc:creator>
      <pubDate>Thu, 01 Oct 2026 07:46:51 +0000</pubDate>
      <link>https://dev.to/andreyschurko/before-you-give-a-polymarket-bot-your-private-key-a-15-minute-checklist-1774</link>
      <guid>https://dev.to/andreyschurko/before-you-give-a-polymarket-bot-your-private-key-a-15-minute-checklist-1774</guid>
      <description>&lt;p&gt;In 2026, "Polymarket copy-trading bot" became one of the most effective lures on GitHub.&lt;/p&gt;

&lt;p&gt;The pattern repeated all year. In February, an attacker took over a legitimate organisation's GitHub account and published more than twenty malicious repositories, several of them Polymarket copy-trading bots. Following their setup instructions installed a hidden npm dependency that read the private key from &lt;code&gt;.env&lt;/code&gt;, sent it to the attacker's server and opened an SSH backdoor (&lt;a href="https://www.stepsecurity.io/blog/malicious-polymarket-bot-hides-in-hijacked-dev-protocol-github-org-and-steals-wallet-keys" rel="noopener noreferrer"&gt;StepSecurity's write-up&lt;/a&gt;). In July, a fake "arbitrage bot" collected dozens of stars and forks before researchers tied it to thirty malicious npm packages.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable part: &lt;strong&gt;a self-hosted trading bot legitimately needs your private key.&lt;/strong&gt; It has to sign orders. So "never give a bot your key" is not useful advice. "Know exactly what the bot does with it" is.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/AndreySchurko/garnet-polymarket" rel="noopener noreferrer"&gt;Garnet&lt;/a&gt;, a self-hosted Polymarket copy-trading engine. Below is the checklist I'd want anyone to run on &lt;em&gt;any&lt;/em&gt; bot, mine included, before putting a funded key into its &lt;code&gt;.env&lt;/code&gt;. It takes about fifteen minutes and needs no security background.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Who asks for what?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A seed phrase is an instant no.&lt;/strong&gt; A bot needs one signing key, never a 12- or 24-word mnemonic. A mnemonic controls every account derived from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hosted service that wants your key is a different trust model&lt;/strong&gt; from software you run yourself. Neither is automatically wrong, but you should know which one you're choosing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promises of returns are a red flag on their own.&lt;/strong&gt; No copy-trading tool can promise profit, because the result depends entirely on the wallets you copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Read the dependency list, not the README
&lt;/h2&gt;

&lt;p&gt;Most stealers in 2026 hid in &lt;strong&gt;dependencies&lt;/strong&gt;, not in the code you'd read. The bot's own code looked clean, and the payload sat in a package installed during setup.&lt;/p&gt;

&lt;p&gt;For a JavaScript or TypeScript project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;package.json                 &lt;span class="c"&gt;# look at every dependency, not just the famous ones&lt;/span&gt;
npm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; | less              &lt;span class="c"&gt;# the full tree you are about to install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for names one letter off from popular packages (&lt;code&gt;big-nunber&lt;/code&gt; instead of &lt;code&gt;bignumber&lt;/code&gt;), packages with almost no downloads, and &lt;code&gt;postinstall&lt;/code&gt; scripts.&lt;/p&gt;

&lt;p&gt;For a Rust project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo tree                       &lt;span class="c"&gt;# the full dependency tree&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;cargo-audit &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo audit   &lt;span class="c"&gt;# known advisories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that a lockfile exists and is committed (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;). Without one, what you install today may differ from what was reviewed yesterday.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Find every place the key is read
&lt;/h2&gt;

&lt;p&gt;Search for the variable name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE_KEY"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.rs"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every hit should lead to &lt;strong&gt;signing&lt;/strong&gt;, and nowhere else. A key that gets read and then concatenated into a string, serialised, logged or passed to an HTTP call is the whole attack in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. List every host the code talks to
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="s1"&gt;'(https?|wss?)://[a-zA-Z0-9.-]+'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Polymarket bot, the expected list is short: Polymarket's own hosts (&lt;code&gt;clob.polymarket.com&lt;/code&gt;, &lt;code&gt;gamma-api&lt;/code&gt;, &lt;code&gt;data-api&lt;/code&gt;, &lt;code&gt;ws-live-data&lt;/code&gt;), a Polygon RPC endpoint, and maybe Telegram's API if it has a Telegram interface. Every other host needs an explanation. Remember that this only covers the bot's own code, which is why step 2 matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check what ends up in the logs
&lt;/h2&gt;

&lt;p&gt;Bots usually print their configuration at startup. Run it without real keys, or with a throwaway key, and read the log. Your key, API secret and passphrase should appear redacted, if at all. If the bot logs them in full, that log file, journald or your hosting provider's console now holds your key.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Start without the key
&lt;/h2&gt;

&lt;p&gt;A well-built bot can do something useful before you hand over a key: paper trading on real market data. If the only way to see it work is to fund a wallet first, you're being asked to trust it blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. When you do go live
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;dedicated wallet&lt;/strong&gt; with only the money you're prepared to lose. Never your main wallet.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;.env&lt;/code&gt; out of git and readable only by you: &lt;code&gt;chmod 600 .env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run it on a server you control, and re-run steps 2–4 after every update.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Garnet answers this checklist
&lt;/h2&gt;

&lt;p&gt;I wrote Garnet's README to be checked against exactly this list, so here are its answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One signing key, never a seed phrase.&lt;/strong&gt; The key lives in &lt;code&gt;.env&lt;/code&gt; on your machine and signs orders locally (EIP-712). The signature goes to the exchange; the key does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No npm.&lt;/strong&gt; The engine is Rust end to end. Every dependency is pinned in &lt;code&gt;Cargo.lock&lt;/code&gt;, and the Polymarket SDK is pinned to an exact version. &lt;code&gt;cargo audit&lt;/code&gt; is clean apart from one advisory in a crate that sits in the lockfile but isn't compiled into any binary, and the README says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key is read in exactly two places&lt;/strong&gt;, both handing it straight to the signer as a &lt;code&gt;SecretString&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The README lists every host the code talks to&lt;/strong&gt;, with the &lt;code&gt;grep&lt;/code&gt; above to verify it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs show `&lt;/strong&gt;&lt;em&gt;REDACTED&lt;/em&gt;&lt;strong&gt;`&lt;/strong&gt;, and a test named &lt;code&gt;the_private_key_never_reaches_a_log_line&lt;/code&gt; holds that line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No keys, no live path.&lt;/strong&gt; With no keys in the environment the live path doesn't start at all, and shadow mode trades on paper on public data, paying the same fees as live would.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to take any of that on trust. That's the point of the checklist.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/AndreySchurko/garnet-polymarket" rel="noopener noreferrer"&gt;https://github.com/AndreySchurko/garnet-polymarket&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The full key-safety section: &lt;a href="https://github.com/AndreySchurko/garnet-polymarket#-your-private-key" rel="noopener noreferrer"&gt;https://github.com/AndreySchurko/garnet-polymarket#-your-private-key&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find a way the key could leak, please report it privately as described in &lt;a href="https://github.com/AndreySchurko/garnet-polymarket/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; instead of in a public issue.&lt;/p&gt;

</description>
      <category>security</category>
      <category>polymarket</category>
      <category>crypto</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
