<?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: tickerseer</title>
    <description>The latest articles on DEV Community by tickerseer (@tickerseer).</description>
    <link>https://dev.to/tickerseer</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%2F4209036%2Fd3c98ce0-d9fa-4fac-92ab-a54e4fd65c03.png</url>
      <title>DEV Community: tickerseer</title>
      <link>https://dev.to/tickerseer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tickerseer"/>
    <language>en</language>
    <item>
      <title>You can't un-archive a page, so guard the Wayback Machine save</title>
      <dc:creator>tickerseer</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:17:00 +0000</pubDate>
      <link>https://dev.to/tickerseer/you-cant-un-archive-a-page-so-guard-the-wayback-machine-save-214c</link>
      <guid>https://dev.to/tickerseer/you-cant-un-archive-a-page-so-guard-the-wayback-machine-save-214c</guid>
      <description>&lt;p&gt;I run &lt;a href="https://tickerseer.com" rel="noopener noreferrer"&gt;tickerseer&lt;/a&gt;, a stock research site. Every week it publishes options trade ideas on companies about to report earnings, and when each one expires the code grades it on a &lt;a href="https://tickerseer.com/track-record" rel="noopener noreferrer"&gt;public ledger&lt;/a&gt;. The page promises that a published call is never edited, withdrawn or removed.&lt;/p&gt;

&lt;p&gt;On its own, that promise doesn't prove much, because I control the server and could change the page whenever I liked. So after each weekly settlement a cron job asks the Internet Archive to save the page. Anyone can open two captures and diff them, and I can't touch either copy.&lt;/p&gt;

&lt;p&gt;That makes the archive the one place where a mistake sticks for good. A capture can't be edited, and getting one removed means asking the Internet Archive for a favour. The save is about ten lines of &lt;code&gt;curl&lt;/code&gt;. Making sure it never stores the wrong page took the other 230 or so lines, most of them comments about the four traps below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: Save Page Now follows redirects
&lt;/h2&gt;

&lt;p&gt;The obvious version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://web.archive.org/save/https://example.com/page"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save Page Now keeps whatever it ends up on. If your page redirects to a login screen, the archive saves the login screen. That happened to us on 15 August 2026: a manual save of a page that needs a paid plan stored our sign-in page in its place.&lt;/p&gt;

&lt;p&gt;The fix is to fetch the page yourself first, without following redirects, and refuse to archive anything that doesn't answer 200 directly:&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="c"&gt;# NEVER add -L here. Following the redirect would read the 200 at the&lt;/span&gt;
&lt;span class="c"&gt;# end of a redirect to a login gate, and then archive the gate.&lt;/span&gt;
&lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 30 &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'\n%{http_code}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;##*&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[wayback] REFUSED &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: answered '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;no&lt;/span&gt;&lt;span class="p"&gt; response&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', not 200"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test checks that &lt;code&gt;-L&lt;/code&gt; stays out of that line, because a stubbed &lt;code&gt;curl&lt;/code&gt; in a unit test can't tell you whether redirects are being followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: a 200 can still be an empty page
&lt;/h2&gt;

&lt;p&gt;Our ledger handler returns 200 on every path. When its data loader fails, it catches the exception, returns an empty list, and the page renders a polite "being prepared" placeholder, also with a 200.&lt;/p&gt;

&lt;p&gt;Archiving that placeholder would be worse than archiving the login page. The page exists to prove nothing was taken down, and the archive would hold a copy showing an empty ledger.&lt;/p&gt;

&lt;p&gt;So the caller passes a marker string that only a populated page contains, and the save is refused without it:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$require&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$require&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[wayback] REFUSED &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: 200 but no '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;require&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wayback_save &lt;span class="s2"&gt;"https://tickerseer.com/track-record"&lt;/span&gt; 120 &lt;span class="s2"&gt;"Overall Win Rate"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 3: no snapshot URL doesn't mean no snapshot
&lt;/h2&gt;

&lt;p&gt;When a save works, Save Page Now usually redirects to the new capture, and you read the URL from the &lt;code&gt;Location&lt;/code&gt; header. Sometimes it answers 200 with an HTML page instead and saves the capture in the background. On 19 September 2026 both of our weekly saves came back with no &lt;code&gt;Location&lt;/code&gt;, and both captures were stored anyway.&lt;/p&gt;

&lt;p&gt;So a missing &lt;code&gt;Location&lt;/code&gt; is a question, and the CDX index can answer it. It tells you which captures of a URL exist, when they were taken and what status code was recorded.&lt;/p&gt;

&lt;p&gt;The catch is that the ledger gets archived every week, so the index is never empty. If you only ask "is there a capture?", the answer is yes forever, including the weeks when the save was broken. The check needs a lower bound, and that bound has to be stamped before the save is requested:&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="nv"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y%m%d%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# before the save, not after&lt;/span&gt;
&lt;span class="c"&gt;# ... request the save ...&lt;/span&gt;

&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 30 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://web.archive.org/cdx/search/cdx?url=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;from=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;since&lt;/span&gt;:0:8&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=-5&amp;amp;fl=timestamp,statuscode"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ts code _&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ts&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[0-9]&lt;span class="o"&gt;{&lt;/span&gt;14&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ts&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$since&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;   &lt;span class="c"&gt;# fixed-width digits compare fine as strings&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://web.archive.org/web/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ts&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;from=&lt;/code&gt; only filters by date, so a capture that lands just after midnight UTC still comes back. The timestamp comparison is the real bound.&lt;/p&gt;

&lt;p&gt;The check only works one way. A hit proves a capture from this run exists. A miss proves nothing, since the capture may just not be indexed yet, so we wait 20 seconds, ask once more, and then log a warning. It can fail to confirm a capture that happened, but it can never confirm one that didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: the index has bad days too
&lt;/h2&gt;

&lt;p&gt;On the day I wrote that check, the CDX endpoint was serving a "Temporarily Offline" HTML page with HTTP 200 and &lt;code&gt;curl&lt;/code&gt; exit status 0. It also timed out at times and sometimes returned an empty body.&lt;/p&gt;

&lt;p&gt;That's why the loop above only accepts a line shaped exactly like &lt;code&gt;&amp;lt;14 digits&amp;gt; &amp;lt;status&amp;gt;&lt;/code&gt;. The HTML outage page has no such line, so it counts as "no answer" and never as a capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bash details that bit along the way
&lt;/h2&gt;

&lt;p&gt;The function is best-effort. An outage at a third party shouldn't kill the weekly settlement job that calls it, and three habits keep it from doing that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local url="${1:?missing}"&lt;/code&gt; looks like input validation, but in a non-interactive shell it exits the whole calling script, and a caller's &lt;code&gt;|| fallback&lt;/code&gt; can't catch that. Use &lt;code&gt;${1-}&lt;/code&gt; and &lt;code&gt;return 2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[[ "$ts" &amp;lt; "$since" ]] &amp;amp;&amp;amp; continue&lt;/code&gt; returns non-zero when the test is false. That's harmless mid-loop, but if it ends up as the last command in a function, the false test becomes the function's return status, and under &lt;code&gt;set -e&lt;/code&gt; an unguarded call site ends the script. &lt;code&gt;if ...; then continue; fi&lt;/code&gt; returns 0 either way.&lt;/li&gt;
&lt;li&gt;Every call site is written &lt;code&gt;wayback_save ... || &amp;lt;log a warning&amp;gt;&lt;/code&gt;, and the two ledger saves also report to Sentry. A failed archive is something to investigate, and it doesn't fail the job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this still can't prove
&lt;/h2&gt;

&lt;p&gt;All of those guards describe what my server sees. The capture that ends up stored is what the archive's crawler saw, from a different IP and a few seconds or minutes later, through a CDN with a firewall in front of it. If a firewall rule ever challenged the crawler, every check above would pass and the archive would store a challenge page.&lt;/p&gt;

&lt;p&gt;The CDX check covers half of that. It proves a capture exists and was stored with a 200, but CDX returns metadata, not content, so it can't tell you the stored body is the ledger. Only opening the capture can tell you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes between two captures
&lt;/h2&gt;

&lt;p&gt;If you diff two captures you will find differences, and the page says where to expect them. The calls stay the same, but the grading changes when I find bugs in the code that reads our own write-ups. In one of them, an entry line reading "Mon 7/6 to Tue 7/7" was parsed as a $7 strike price, so a $62 stock could never lose the trade. Each fix gets a dated entry under &lt;a href="https://tickerseer.com/track-record#methodology" rel="noopener noreferrer"&gt;Corrections&lt;/a&gt; with its effect on the total, and the ones that helped our numbers get the same detail as the ones that hurt. With the archive in place, a bug like that can't be fixed quietly without someone being able to see it.&lt;/p&gt;

&lt;p&gt;The helper ended up at about 240 lines of Bash, and most of them are comments. Each one records an incident like these, so whoever touches the file next knows why a guard is there before deleting it.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
