<?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>Forem: Kamil Kujawiński</title>
    <description>The latest articles on Forem by Kamil Kujawiński (@kkuj).</description>
    <link>https://forem.com/kkuj</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2458391%2Fdea0ef15-f5f2-41b3-9e81-8c2a87ee8685.jpeg</url>
      <title>Forem: Kamil Kujawiński</title>
      <link>https://forem.com/kkuj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kkuj"/>
    <language>en</language>
    <item>
      <title>Search Sofascore from Your Browser's Address Bar</title>
      <dc:creator>Kamil Kujawiński</dc:creator>
      <pubDate>Tue, 21 Apr 2026 21:21:18 +0000</pubDate>
      <link>https://forem.com/kkuj/search-sofascore-from-your-browsers-address-bar-56e0</link>
      <guid>https://forem.com/kkuj/search-sofascore-from-your-browsers-address-bar-56e0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrm5ef8mb8yw2ftcutcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrm5ef8mb8yw2ftcutcu.png" alt=" " width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://digit11.com/blog/sofascore-search/sofascore_search_engine.gif" rel="noopener noreferrer"&gt;Sofascore browser search - GIF example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I watch a lot of football and I'm constantly looking things up on &lt;a href="https://www.sofascore.com" rel="noopener noreferrer"&gt;Sofascore&lt;/a&gt;. Opening the site, clicking the magnifier, typing — it adds up. What I want is: type &lt;code&gt;sofa barcelona&lt;/code&gt; in my address bar, hit Enter, land on the search results.&lt;/p&gt;

&lt;p&gt;Sofascore's URL doesn't accept a &lt;code&gt;?q=&lt;/code&gt; parameter natively, and the search input is a React-controlled field that ignores programmatically-set values. Getting from the address bar to a live Sofascore search takes three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;browser keyword shortcut&lt;/strong&gt; that routes &lt;code&gt;sofa &amp;lt;query&amp;gt;&lt;/code&gt; to Sofascore with the query attached as &lt;code&gt;?q=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Tampermonkey userscript&lt;/strong&gt; that reads &lt;code&gt;?q=&lt;/code&gt; on page load and feeds it into the search box&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;local typing service&lt;/strong&gt; that actually types the query, key by key, because the React input won't accept anything less&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Add the browser keyword
&lt;/h3&gt;

&lt;p&gt;Chrome (and any Chromium browser) supports address-bar search keywords. Add one pointing at Sofascore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open &lt;code&gt;chrome://settings/searchEngines&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt; under "Site search"&lt;/li&gt;
&lt;li&gt;Set:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: Sofascore&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shortcut&lt;/strong&gt;: &lt;code&gt;sofa&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;: &lt;code&gt;https://www.sofascore.com/pl/?q=%s&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Firefox and Safari have equivalent settings — the important part is the &lt;code&gt;%s&lt;/code&gt; placeholder, which the browser replaces with whatever you typed after the keyword.&lt;/p&gt;

&lt;p&gt;Now typing &lt;code&gt;sofa barcelona&lt;/code&gt; in the address bar navigates to &lt;code&gt;https://www.sofascore.com/pl/?q=barcelona&lt;/code&gt;. Sofascore itself ignores the &lt;code&gt;?q=&lt;/code&gt;; the userscript picks it up from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Userscript: turn ?q= into a real search
&lt;/h3&gt;

&lt;p&gt;Install &lt;a href="https://www.tampermonkey.net/" rel="noopener noreferrer"&gt;Tampermonkey&lt;/a&gt;, then add this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ==UserScript==&lt;/span&gt;
&lt;span class="c1"&gt;// @name         Websearch for Sofascore&lt;/span&gt;
&lt;span class="c1"&gt;// @namespace    https://digit11.com/&lt;/span&gt;
&lt;span class="c1"&gt;// @version      1.0.0&lt;/span&gt;
&lt;span class="c1"&gt;// @description  Forward ?q= search terms into Sofascore's search input via Real Type&lt;/span&gt;
&lt;span class="c1"&gt;// @author       kkujawinski&lt;/span&gt;
&lt;span class="c1"&gt;// @match        https://www.sofascore.com/*&lt;/span&gt;
&lt;span class="c1"&gt;// @icon         https://www.google.com/s2/favicons?sz=64&amp;amp;domain=sofascore.com&lt;/span&gt;
&lt;span class="c1"&gt;// @grant        none&lt;/span&gt;
&lt;span class="c1"&gt;// ==/UserScript==&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&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;TYPING_SERVER_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8765&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;realType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;focusFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Focus the current element if requested&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;focusFirst&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Small delay to ensure focus&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Send to macOS typing service&lt;/span&gt;
        &lt;span class="k"&gt;try&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;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TYPING_SERVER_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/type&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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;body&lt;/span&gt;&lt;span class="p"&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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server responded with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;result&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;response&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Typing completed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&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;result&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to connect to typing service:&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Make sure the macOS typing service is running on port 8765&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getQueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;urlParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&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;urlParams&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getQueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchValue&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;simulateFocusClickLoop&lt;/span&gt;&lt;span class="p"&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;input&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;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input#search-input&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="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="o"&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;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.z_dropdown input[type="radio"][name="all"]&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="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="o"&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;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focusing...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&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;realType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// input.value = searchValue;&lt;/span&gt;
            &lt;span class="c1"&gt;// input.dispatchEvent(new InputEvent('input', { bubbles: true }));&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the &lt;code&gt;q&lt;/code&gt; query parameter (with a trailing-slash guard for browsers that append one).&lt;/li&gt;
&lt;li&gt;Polls for &lt;code&gt;input#search-input&lt;/code&gt; to appear, then focuses and clicks it in a loop until the search dropdown (&lt;code&gt;.z_dropdown input[type="radio"][name="all"]&lt;/code&gt;) shows up — Sofascore's signal that the input is ready to accept keystrokes.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;realType(searchValue)&lt;/code&gt;, which forwards the query to a local typing server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why not just &lt;code&gt;input.value = searchValue&lt;/code&gt;? That's the commented-out line at the bottom — it's what you reach for first, and it doesn't work. React's controlled-input handlers ignore the direct assignment, and dispatching a synthetic &lt;code&gt;InputEvent&lt;/code&gt; gets filtered out too. The search input stays empty.&lt;/p&gt;

&lt;p&gt;The typing server exists for exactly this reason. That's the last piece.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Real Type — typing like a real keyboard
&lt;/h3&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/kkujawinski/real-type-server" rel="noopener noreferrer"&gt;kkujawinski/real-type-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AppleScript doing the work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core is a handful of AppleScript lines driving System Events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System Events"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;textToType&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;currentChar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;character&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;textToType&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;keystroke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;currentChar&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;charDelay&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keystroke&lt;/code&gt; is the important bit. Unlike pasting or setting a field's value, it emits events indistinguishable from a real keyboard. The target app sees key-down / key-up events arriving at human pace, so input validators, focus handlers — all of it just works. Sofascore's React input included.&lt;/p&gt;

&lt;p&gt;The standalone &lt;code&gt;macos-typer.applescript&lt;/code&gt; file wraps this loop so you can invoke it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osascript macos-typer.applescript &lt;span class="s2"&gt;"hello world"&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A tiny Python server in front&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To make the typer callable from a browser userscript — or a shell alias, or another machine on localhost — there's a thin Python HTTP server (&lt;code&gt;osascript-typing-server.py&lt;/code&gt;) on port 8765. It's &lt;code&gt;http.server&lt;/code&gt; from the stdlib, no dependencies.&lt;/p&gt;

&lt;p&gt;It accepts one route, &lt;code&gt;POST /type&lt;/code&gt;, reads &lt;code&gt;{ "text", "delay" }&lt;/code&gt; from the JSON body, escapes the text for AppleScript, shells out to &lt;code&gt;osascript -e …&lt;/code&gt;, and returns a JSON status. You can call it straight from a webpage — exactly what the userscript does.&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;-X&lt;/span&gt; POST http://localhost:8765/type &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text": "Hello, world!", "delay": 50}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole server. The value isn't in the Python — it's in giving the AppleScript a stable local endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autostart at login&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;install-autostart.sh&lt;/code&gt; writes a LaunchAgent plist to &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt; and &lt;code&gt;launchctl&lt;/code&gt; loads it. From then on the server boots with your user session, restarts automatically if it crashes, and logs to &lt;code&gt;/tmp/osascript-typing-server.log&lt;/code&gt;. One-time setup: grant Accessibility permission to the Python binary the installer reports, otherwise &lt;code&gt;keystroke&lt;/code&gt; silently no-ops.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uninstall-autostart.sh&lt;/code&gt; and &lt;code&gt;check-status.sh&lt;/code&gt; handle the rest of the lifecycle.&lt;/p&gt;

&lt;p&gt;Full source, install scripts, and the LaunchAgent plist are in the &lt;a href="https://github.com/kkujawinski/real-type-server" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting it together
&lt;/h3&gt;

&lt;p&gt;With the three pieces in place, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;sofa barcelona&lt;/code&gt; in the address bar, hit Enter.&lt;/li&gt;
&lt;li&gt;Browser navigates to &lt;code&gt;https://www.sofascore.com/pl/?q=barcelona&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Userscript waits for the search input to be ready, then hands the query off to the typing server.&lt;/li&gt;
&lt;li&gt;Real Type types &lt;code&gt;barcelona&lt;/code&gt; into the focused field at human pace.&lt;/li&gt;
&lt;li&gt;Sofascore's search dropdown fills with live results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same pattern works for any site whose search resists synthetic input — swap the selector and the keyword URL, and you've got another shortcut.&lt;/p&gt;

</description>
      <category>sofascore</category>
      <category>browser</category>
      <category>osx</category>
    </item>
    <item>
      <title>Monitor Google Cloud Platform Costs on Your iPhone and Apple Watch</title>
      <dc:creator>Kamil Kujawiński</dc:creator>
      <pubDate>Mon, 23 Mar 2026 18:51:37 +0000</pubDate>
      <link>https://forem.com/kkuj/monitor-google-cloud-platform-costs-on-your-iphone-and-apple-watch-420f</link>
      <guid>https://forem.com/kkuj/monitor-google-cloud-platform-costs-on-your-iphone-and-apple-watch-420f</guid>
      <description>&lt;p&gt;Keep an eye on your GCP costs right from your wrist. Don't let autoscaling silently run up your bill — stay informed with a quick glance at your iPhone or Apple Watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the plan:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure GCP billing export to a BigQuery table&lt;/li&gt;
&lt;li&gt;Create an n8n workflow that queries BigQuery for cost data&lt;/li&gt;
&lt;li&gt;Set up a widget in API Widgets to display the results&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure GCP Billing Export to BigQuery
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Enable the BigQuery API in GCP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5276ackltexkp53440mv.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5276ackltexkp53440mv.jpeg" alt="Enable BigQuery API in the GCP console" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enable billing export to BigQuery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5qintr2p0qubysq8ivd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5qintr2p0qubysq8ivd.jpeg" alt="Enable billing export to BigQuery" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create a service account for BigQuery access&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create n8n-bigquery-reader &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"n8n BigQuery Reader"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Assign minimal permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The service account needs just two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bigquery.jobUser&lt;/code&gt; — allows running queries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bigquery.dataViewer&lt;/code&gt; — allows reading tables and views (read-only)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow running queries and reading results&lt;/span&gt;
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:n8n-bigquery-reader@YOUR_PROJECT_ID.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/bigquery.jobUser"&lt;/span&gt;

&lt;span class="c"&gt;# Allow reading data from datasets and tables&lt;/span&gt;
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:n8n-bigquery-reader@YOUR_PROJECT_ID.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/bigquery.dataViewer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Generate a service account key for n8n&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts keys create n8n-bigquery-key.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n-bigquery-reader@YOUR_PROJECT_ID.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a &lt;code&gt;n8n-bigquery-key.json&lt;/code&gt; file containing the service account credentials. You'll use this file to authenticate the BigQuery node in n8n.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the n8n Workflow
&lt;/h3&gt;

&lt;p&gt;The workflow exposes a webhook endpoint that API Widgets can call. When triggered, it queries BigQuery for your billing data, groups costs by date and service, and returns the results as JSON.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bnrycuaij6xqqy1rljj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bnrycuaij6xqqy1rljj.jpeg" alt="n8n workflow for querying GCP billing costs" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow consists of five nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Webhook&lt;/strong&gt; — listens for incoming requests (accepts an optional &lt;code&gt;days&lt;/code&gt; query parameter, defaults to 14)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure variables&lt;/strong&gt; — sets the BigQuery table name and the number of days to query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute BigQuery&lt;/strong&gt; — runs a SQL query that aggregates daily costs by service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format data&lt;/strong&gt; — transforms the results into a structured JSON response with daily totals and per-service breakdowns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond to Webhook&lt;/strong&gt; — sends the formatted data back to the caller&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can import the workflow into your n8n instance using the JSON file: &lt;a href="https://digit11.com/blog/gcp-cost/GCP%20Billing%20costs.json" rel="noopener noreferrer"&gt;GCP Billing costs.json&lt;/a&gt;. Make sure to update the &lt;code&gt;table_name&lt;/code&gt; variable in the "Configure variables" node to match your BigQuery billing export table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure API Widgets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; Download &lt;a href="https://apps.apple.com/us/app/api-widgets/id6756238482" rel="noopener noreferrer"&gt;API Widgets&lt;/a&gt; from the App Store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; Create a new widget and configure the API endpoint in the &lt;strong&gt;Source&lt;/strong&gt; tab, pointing it to your n8n webhook URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; Set up the visualization in the &lt;strong&gt;Design&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fatj4e4p8qhz8iwml51sd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fatj4e4p8qhz8iwml51sd.jpeg" alt="API Widgets design configuration on iPhone - screenshot" width="591" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; Add a home screen widget and link it to the one you just created.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pw88o2hmd73ed8dmxvu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pw88o2hmd73ed8dmxvu.jpeg" alt="iPhone home screen with GCP cost widget - screenshot" width="591" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74je4hb2qp3gjfci6ofc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74je4hb2qp3gjfci6ofc.jpeg" alt="iPhone home screen with GCP cost widget bar details - screenshot" width="800" height="847"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The widget also works on Apple Watch — view your costs at a glance from your wrist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwv5qgawzdr9v8tpuat.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwv5qgawzdr9v8tpuat.jpeg" alt="Apple Watch home screen with GCP cost widget - screenshot" width="416" height="496"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqfuctdcfh9njc42f5q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqfuctdcfh9njc42f5q.jpeg" alt="Apple Watch widget showing GCP cost chart - screenshot" width="416" height="496"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9uvu9wh1vnyt5mjvj1q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9uvu9wh1vnyt5mjvj1q.jpeg" alt="Apple Watch widget showing cost breakdown details - screenshot" width="416" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>iphone</category>
      <category>n8n</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Using Cloudflare SSL with Elastic Beanstalk instances</title>
      <dc:creator>Kamil Kujawiński</dc:creator>
      <pubDate>Sat, 04 Jan 2025 16:06:04 +0000</pubDate>
      <link>https://forem.com/kkuj/using-cloudflare-ssl-with-elastic-beanstalk-instances-gf3</link>
      <guid>https://forem.com/kkuj/using-cloudflare-ssl-with-elastic-beanstalk-instances-gf3</guid>
      <description>&lt;p&gt;Motivation:&lt;/p&gt;

&lt;p&gt;To obtain a free SSL certificate (by Let's Encrypt) for your web application without the hassle of renewing it every three months, consider the following approach.&lt;/p&gt;

&lt;p&gt;The simplest solution is to use a Load Balancer and attach an SSL certificate from AWS Certificate Manager (ACM) to it. However, relying on a &lt;strong&gt;Load Balancer solely for HTTPS may be costly&lt;/strong&gt;. To address this, I’ve outlined a much more affordable alternative that requires a bit of configuration.&lt;/p&gt;

&lt;p&gt;Additionally, in this post, I explain how to configure SSL for an Elastic Beanstalk instance, as I found the existing documentation and available resources to be somewhat outdated.&lt;/p&gt;




&lt;p&gt;Keyword of the solution is Cloudflare.&lt;/p&gt;

&lt;p&gt;Cloudflare offers a proxy feature that enables you to set up a free SSL certificate while securely proxying traffic to your host.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Flexible&lt;/strong&gt; mode, the communication between Cloudflare and AWS is not encrypted. If you prefer end-to-end encryption, you can opt for &lt;strong&gt;Full&lt;/strong&gt; mode instead.&lt;/p&gt;

&lt;p&gt;Steps&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add your domain to Cloudflare.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the DNS settings, configure your domain as proxied.&lt;/li&gt;
&lt;li&gt;In the SSL/TLS settings, select Full (strict) mode for end-to-end encryption.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Generate an Origin Certificate in Cloudflare
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to the SSL/TLS &amp;gt; Origin Server section in Cloudflare.&lt;/li&gt;
&lt;li&gt;Generate a certificate valid for up to 15 years.&lt;/li&gt;
&lt;li&gt;Use this certificate to configure SSL in AWS Elastic Beanstalk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Store Certificates in AWS Secrets Manager:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Save the &lt;code&gt;server.crt&lt;/code&gt; and &lt;code&gt;server.key&lt;/code&gt; files as secrets in AWS Secrets Manager.&lt;/li&gt;
&lt;li&gt;Use the following secret names: &lt;code&gt;/app/ssl/server-crt&lt;/code&gt; and &lt;code&gt;/app/ssl/server-key&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Grant IAM Role Access to Secrets:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In AWS IAM add a new policy named &lt;code&gt;ReadSSLSecretKeys&lt;/code&gt; for the role &lt;code&gt;aws-elasticbeanstalk-ec2-role&lt;/code&gt; with the following permissions:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:region:account_id:parameter/app/ssl/server-crt"
    },
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:region:account_id:parameter/app/ssl/server-key"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Configure your Elastic Beanstalk deployment to listen on port &lt;code&gt;443&lt;/code&gt; and use the Origin Certificate generated in Cloudflare (stored in AWS Secrets Manager) do changes in the following files:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.ebextensions/01_https-instance-securitygroup.config&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resources:
  sslSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
      CidrIp: 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.ebextensions/02_https-instance.config&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files:
  /etc/pki/tls/certs/server.crt:
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN CERTIFICATE-----
      certificate file contents
      -----END CERTIFICATE-----

  /etc/pki/tls/certs/server.key:
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN RSA PRIVATE KEY-----
      private key contents # See note below.
      -----END RSA PRIVATE KEY-----

container_commands:
  01_fetch_server_crt:
    command: |
      aws ssm get-parameter --name "/app/ssl/server-crt" --with-decryption --query "Parameter.Value" --output text &amp;gt; /etc/pki/tls/certs/server.crt
  02_fetch_server_key:
    command: |
      aws ssm get-parameter --name "/app/ssl/server-key" --with-decryption --query "Parameter.Value" --output text &amp;gt; /etc/pki/tls/certs/server.key

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.platform/nginx/conf.d/https.conf&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 443 ssl;

    ssl_certificate /etc/pki/tls/certs/server.crt;
    ssl_certificate_key /etc/pki/tls/certs/server.key;

    ssl_session_timeout 5m;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    location / {
      proxy_pass http://127.0.0.1:8000;
      proxy_http_version 1.1;

      proxy_set_header Connection "";
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto https;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>aws</category>
      <category>elasticbeanstalk</category>
      <category>cloudflare</category>
      <category>ssl</category>
    </item>
  </channel>
</rss>
