<?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: Alligator Peach</title>
    <description>The latest articles on DEV Community by Alligator Peach (@alligator_peach_developer).</description>
    <link>https://dev.to/alligator_peach_developer</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%2F4088580%2F35cbeb5a-5a5b-48c0-8f94-a9101a68b772.jpeg</url>
      <title>DEV Community: Alligator Peach</title>
      <link>https://dev.to/alligator_peach_developer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alligator_peach_developer"/>
    <language>en</language>
    <item>
      <title>I Built a Crypto Market Intelligence App with React Native, Supabase &amp; Cloudflare Workers AI</title>
      <dc:creator>Alligator Peach</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:59:05 +0000</pubDate>
      <link>https://dev.to/alligator_peach_developer/i-built-a-crypto-market-intelligence-app-with-react-native-supabase-cloudflare-workers-ai-4j0e</link>
      <guid>https://dev.to/alligator_peach_developer/i-built-a-crypto-market-intelligence-app-with-react-native-supabase-cloudflare-workers-ai-4j0e</guid>
      <description>&lt;h2&gt;
  
  
  The problem: alert fatigue
&lt;/h2&gt;

&lt;p&gt;Keeping up with crypto meant juggling five different apps — a price tracker, a scanner, news feeds, sentiment charts, and a stack of alert apps. And even then I kept missing the moves that mattered.&lt;/p&gt;

&lt;p&gt;The existing tools fell into two camps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too noisy&lt;/strong&gt; — thousands of alerts, most of them irrelevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too shallow&lt;/strong&gt; — pretty charts without any real analysis behind them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;SignalWatch&lt;/strong&gt;: a mobile app that turns overwhelming market data into a clear, prioritized signal board. It scans 100+ coins, understands plain-English questions, tracks market sentiment, and only alerts you about assets that actually pass the bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;React Native (Expo SDK 54), TypeScript, React Navigation 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth &amp;amp; Sync&lt;/td&gt;
&lt;td&gt;Supabase (Google OAuth + email/password)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysis engine&lt;/td&gt;
&lt;td&gt;Node/Express on Cloud Run — scheduled every 15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data ingestion&lt;/td&gt;
&lt;td&gt;Cloudflare Workers (CoinMarketCap + Binance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI semantic scanner&lt;/td&gt;
&lt;td&gt;Cloudflare Workers AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;FCM via a Supabase Edge Function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market data&lt;/td&gt;
&lt;td&gt;Binance &amp;amp; CoinMarketCap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│         React Native App (Expo)             │
│  Dashboard · Scanner · Signal · Calendar    │
└───────┬──────────────────┬──────────────────┘
        │ Supabase queries │ POST /api/ai-query (Bearer JWT)
        ▼                  ▼
  ┌────────────┐    ┌──────────────────┐
  │  Supabase  │    │ Cloudflare Worker│
  │  (Postgres)│    │  Workers AI      │
  └─────▲──────┘    └────────▲─────────┘
        │                    │
┌───────┴────────┐   ┌───────┴────────┐
│  Cloud Run     │   │  Cloudflare    │
│  (analysis,    │   │  Worker        │
│   every 15min) │   │  (CMC ingest)  │
└───────▲────────┘   └───────▲────────┘
        │                    │
        └──── Binance + CoinMarketCap ────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two services feed a shared Supabase database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run backend&lt;/strong&gt; — technical analysis (RSI, MACD, EMA, Bollinger Bands, SuperTrend) across futures &amp;amp; spot markets, plus economic calendar events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Worker&lt;/strong&gt; — CoinMarketCap ingestion (top 100, trending, fear &amp;amp; greed, news) and the AI query endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app itself is architected with thin screens over a central hook (&lt;code&gt;useDashboard&lt;/code&gt;) that acts as the single source of truth, with a clean service layer for all data access.&lt;/p&gt;




&lt;h2&gt;
  
  
  The feature I'm most proud of: the AI semantic scanner
&lt;/h2&gt;

&lt;p&gt;Users can ask questions in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"show me coins with RSI &amp;lt; 30 and volume spike &amp;gt; 100%"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The query goes to a Cloudflare Worker, which detects intent, pulls real data from Supabase, and lets Workers AI compose a structured, data-backed answer. No hardcoded results, no fake confidence scores.&lt;/p&gt;

&lt;p&gt;One subtle but critical detail: the app authenticates to the worker with the user's Supabase access token (&lt;code&gt;Bearer&lt;/code&gt;), and the worker validates it against the Supabase JWKS. No secrets are ever embedded in the app binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/ai.ts — get a valid token, then call the worker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getValidAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;userData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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;refreshed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshSession&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refreshError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;refreshData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&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;refreshData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;h3&gt;
  
  
  ⚠️ The ES256 vs RS256 gotcha
&lt;/h3&gt;

&lt;p&gt;This project taught me a lesson: &lt;strong&gt;not all Supabase JWTs are RS256&lt;/strong&gt;. This particular project signs tokens with &lt;strong&gt;ES256 (EC P-256)&lt;/strong&gt;. My worker's JWT verifier originally only handled RS256, so &lt;em&gt;every&lt;/em&gt; valid token was rejected with a confusing 401 — "Please sign in" — even though the user was clearly signed in.&lt;/p&gt;

&lt;p&gt;The fix was to support both ES256 and RS256 in the worker's verification path, plus logging exactly &lt;em&gt;why&lt;/em&gt; verification failed (much easier to debug than a generic 401).&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons learned along the way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. External sites will block your cloud IPs
&lt;/h3&gt;

&lt;p&gt;ForexFactory (for the economic calendar's news feed) returns &lt;strong&gt;HTTP 403 to Cloud Run datacenter IPs&lt;/strong&gt;. The calendar HTML page works fine, but the news scrape doesn't. My solution: a graceful &lt;strong&gt;fallback&lt;/strong&gt; — if the &lt;code&gt;forex_news&lt;/code&gt; table is empty or the request fails, the app falls back to CoinMarketCap news (&lt;code&gt;news_insights&lt;/code&gt;), which is populated by the Cloudflare Worker. The section is never empty, and users can't tell the difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/marketCalendar.ts — fallback to CMC news when forex news fails&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchForexNews&lt;/span&gt; &lt;span class="o"&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;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ForexNewsRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forex_news&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published_at&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="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forexNews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ForexNewsRow&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;forexNews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;forexNews&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...fallback to news_insights (CoinMarketCap via worker)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Data freshness is a feature
&lt;/h3&gt;

&lt;p&gt;A pipeline that ingests every 15 minutes across multiple timeframes (15m / 1h / 4h / 1d) means the "now" is actually &lt;em&gt;now&lt;/em&gt;. That matters enormously in fast-moving markets — and it's a real competitive differentiator, not a buzzword.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Alert fatigue is the real enemy
&lt;/h3&gt;

&lt;p&gt;Most tools push &lt;em&gt;volume&lt;/em&gt;, not &lt;em&gt;signal&lt;/em&gt;. SignalWatch bakes in quality filters so users only get notified about assets that actually pass the bar. It sounds obvious, but it's the thing that makes the product feel calm instead of chaotic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep the app architecture boring
&lt;/h3&gt;

&lt;p&gt;Thin screens → one central hook → a service layer → Supabase/worker. It kept a solo developer sane, made the codebase easy to grow, and made the light/dark theme + EN/ID localization trivial to add.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎯 AI scanner you can talk to (Cloudflare Workers AI over live data)&lt;/li&gt;
&lt;li&gt;📊 Multi-timeframe technical analysis on 100+ coins&lt;/li&gt;
&lt;li&gt;🌍 Global sentiment (Fear &amp;amp; Greed, bullish ratio, market score)&lt;/li&gt;
&lt;li&gt;🔔 Quality-filtered push notifications (no spam)&lt;/li&gt;
&lt;li&gt;⭐ Synced watchlist, 📅 economic calendar, 📰 impact-labeled news&lt;/li&gt;
&lt;li&gt;🌗 Light/dark + EN/ID, fully customizable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently available on &lt;strong&gt;Google Play&lt;/strong&gt; with all Pro features free. iOS is next (the codebase is Expo, so the path is clear).&lt;/p&gt;

&lt;p&gt;If you've hit similar walls with external APIs blocking cloud IPs, or JWT algorithms that don't match the docs — I'd love to hear how you solved it. Feedback and feature ideas are very welcome. 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.builtwithme.signalwatch" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>supabase</category>
      <category>cloudflare</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
