<?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: PSGtechnauts</title>
    <description>The latest articles on DEV Community by PSGtechnauts (@psgtechnautsnl).</description>
    <link>https://dev.to/psgtechnautsnl</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%2Forganization%2Fprofile_image%2F10917%2F59fd12c3-457b-4be5-a80a-1e6bdacc161a.png</url>
      <title>DEV Community: PSGtechnauts</title>
      <link>https://dev.to/psgtechnautsnl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/psgtechnautsnl"/>
    <language>en</language>
    <item>
      <title>Keyboard Ergonomics for Software Engineers: Down the Keyboard Rabbit Hole</title>
      <dc:creator>Sharlon Regales</dc:creator>
      <pubDate>Thu, 12 Mar 2026 19:14:30 +0000</pubDate>
      <link>https://dev.to/psgtechnautsnl/keyboard-ergonomics-for-software-engineers-down-the-keyboard-rabbit-hole-pl7</link>
      <guid>https://dev.to/psgtechnautsnl/keyboard-ergonomics-for-software-engineers-down-the-keyboard-rabbit-hole-pl7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
Keyboard Ergonomics for Software Engineers: Down the Keyboard Rabbit Hole

&lt;ul&gt;
&lt;li&gt;A Brief History of Why Your Keyboard Looks Like That&lt;/li&gt;
&lt;li&gt;Blame the Typewriter&lt;/li&gt;
&lt;li&gt;Keys That Were Never On the Typewriter&lt;/li&gt;
&lt;li&gt;My Journey: A Guided Tour of Increasingly Extreme Decisions&lt;/li&gt;
&lt;li&gt;Step 1: Try a Different Layout&lt;/li&gt;
&lt;li&gt;Step 2: Make the Keyboard First-Class&lt;/li&gt;
&lt;li&gt;Step 3: Program the Keyboard You Already Have&lt;/li&gt;
&lt;li&gt;Step 4: Get a Proper Ergonomic Keyboard&lt;/li&gt;
&lt;li&gt;Step 5: Join the Open-Source Community&lt;/li&gt;
&lt;li&gt;Step 6: The Bottom of the Rabbit Hole&lt;/li&gt;
&lt;li&gt;Conclusion: What I Know Now&lt;/li&gt;
&lt;li&gt;Where to Start&lt;/li&gt;
&lt;li&gt;My Layout&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why your keyboard is shaped the way it is&lt;/li&gt;
&lt;li&gt;Why that shape causes cumulative strain&lt;/li&gt;
&lt;li&gt;How to improve your setup incrementally, from software to custom hardware&lt;/li&gt;
&lt;li&gt;Why fewer keys reduce strain&lt;/li&gt;
&lt;li&gt;What a minimal 34-key layout looks like&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;I've been a track and field athlete since I was eight years old. My event, javelin, demands full shoulder mobility, rotational power, and precise joint mechanics built over years. My career, software engineering, runs almost entirely counter to that: stationary, through a keyboard, for hours a day.&lt;/p&gt;

&lt;p&gt;This difference brought friction, but I dismissed the early warnings; recurring aches and pains. I told myself it's the training load, stress, and/or irregular sleep. Then came the season-ending injuries: first shoulder, then an elbow, then the shoulder once more.&lt;/p&gt;

&lt;p&gt;After the third, my physical therapist was direct: my injuries were symptoms of a bigger problem. The way I was sitting at my keyboard (shoulders rolled forward, wrists deviated, neck craned) was progressively undermining the same joint mechanics that javelin demands. My desk work was undoing years of training. The keyboard I'd never thought about was interfering with the thing I cared about most.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Brief History of Why Your Keyboard Looks Like That
&lt;/h2&gt;

&lt;p&gt;To understand why your hands land where they do, you need to go back to the 1870s. Sit at a standard keyboard and notice: forearms flat, wrists angled inward, shoulders rolled forward. That's not a bad habit. That's the keyboard working as designed. It had been loading my joints in exactly the wrong way, every day, for years.&lt;/p&gt;

&lt;p&gt;This matters beyond athletics. Anyone who types for a living spends thousands of hours a year in the positions that keyboard forces. The cumulative load is the same whether you're training for javelin or sitting in a meeting.&lt;/p&gt;

&lt;p&gt;Which raised the question: why does the keyboard demand this in the first place?&lt;/p&gt;

&lt;h3&gt;
  
  
  Blame the Typewriter
&lt;/h3&gt;

&lt;p&gt;QWERTY was designed in the 1870s by Christopher Latham Sholes for the Remington mechanical typewriter. Each key was attached to a physical metal arm called a type bar, which swung up to stamp ink onto paper. All these arms were arranged in a semicircular arc below the paper roller, converging at a single strike point.&lt;/p&gt;

&lt;p&gt;Here's the problem: if you typed two adjacent letters quickly, adjacent type bars would collide and jam. Sholes's solution was to rearrange letters so their bars were less likely to physically crash into each other. The letter arrangement we call QWERTY is, at its core, a mechanical compromise for 19th-century metalwork.&lt;/p&gt;

&lt;p&gt;The staggered rows, where each row is offset diagonally from the one below, is a similar story. Linkages between the keys and the type bars needed physical clearance to avoid interference. The stagger is a hardware constraint from a machine that no longer exists, preserved faithfully on every keyboard manufactured since.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Row-stagger (Remington No. 2, 1878):
2 3 4 5 6 7 8 9
  Q W E R T Y U I O P
   A S D F G H J K L ; M
     Z C X V B N

no 1 or 0 keys; typists used lowercase l and uppercase O
M sits after L, not below N; X and C are swapped vs. modern QWERTY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engineering problem was solved. The layout never changed. Once enough typists and typewriters used QWERTY, switching became economically irrational, even if a better alternative existed. When computing arrived, it inherited the layout wholesale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keys That Were Never On the Typewriter
&lt;/h3&gt;

&lt;p&gt;While we're here, at the dawn of the digital age: a lot of keys on your keyboard weren't on the original typewriter. The Escape key was added by MIT engineers in 1960 for early terminal systems. Function keys came from IBM. Media keys arrived with the multimedia PCs in the 1990s. Modifier keys (Shift, Control, Alt, and Super/Windows/Command key with its rotating cast of logos) crept onto the layout over the decades as software demanded new input combinations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Esc  F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12  PrtSc  ScrLk  Pause

  ~  .  .  .  .  .  .  .  .  .  .  .  .  Bksp    Ins   Home  PgUp
  Tab  .  .  .  .  .  .  .  .  .  .  .  .  .     Del   End   PgDn
  Caps  .  .  .  .  .  .  .  .  .  .  .   Ent
  Shft   .  .  .  .  .  .  .  .  .  .    Shft            ↑
  Ctrl  Win  Alt   Spc   Alt  Win  Menu  Ctrl        ←  ↓  →

. = letter, number, or symbol key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody ever sat down to design a keyboard for the digital era. We inherited a mechanical instrument built for stamping ink onto paper, bolted on new keys as software demanded them, and called it done. The interface was never reconsidered. Just patched.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Journey: A Guided Tour of Increasingly Extreme Decisions
&lt;/h2&gt;

&lt;p&gt;That's the inheritance. What follows is what I did about it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE] I never measured any of this: no baseline before I started, no controlled comparison between steps. Every step was a response to a physical problem the previous one hadn't fully resolved. The sequence runs from least to most disruptive: if you know what's wrong, skip to it. I went all the way because the shoulder didn't give me many other options.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 1: Try a Different Layout
&lt;/h3&gt;

&lt;p&gt;My first instinct was the same one most people have: if the layout is the problem, why not just use a better one? Turns out, &lt;a href="https://getreuer.info/posts/keyboards/alt-layouts/index.html" rel="noopener noreferrer"&gt;alt layouts&lt;/a&gt; exist and people have &lt;em&gt;very&lt;/em&gt; strong opinions on them.&lt;/p&gt;

&lt;p&gt;Each layout has genuine engineering reasoning behind it; they're not arbitrary alternatives to QWERTY, they're deliberate redesigns. The value isn't in the statistics: it's whether the design philosophy resonates with how you work, and whether you're willing to commit to the relearning. Before that commitment, think about your working context. You may have constraints beyond personal preference. The layout you pick will be the hardest decision to reverse.&lt;/p&gt;

&lt;p&gt;The most established alternative is &lt;a href="https://dvorak-keyboard.com/" rel="noopener noreferrer"&gt;Dvorak&lt;/a&gt; (1936): common English letters on the home row (the middle row, where your fingers rest between keystrokes), hands alternating for most sequences, minimal finger travel. It's baked into every major operating system, and you can switch right now without buying anything.&lt;/p&gt;

&lt;p&gt;The difference is visible at a glance: notice how Dvorak loads the home row with the most common letters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  QWERTY:                          Dvorak:
  1  2  3  4  5  6  7  8  9  0    1  2  3  4  5  6  7  8  9  0
  Q  W  E  R  T  Y  U  I  O  P    '  ,  .  P  Y  F  G  C  R  L
  A  S  D  F  G  H  J  K  L  ;    A  O  E  U  I  D  H  T  N  S   &amp;lt;- home row
  Z  X  C  V  B  N  M  ,  .  /    ;  Q  J  K  X  B  M  W  V  Z

  Dvorak Classic:
  7  5  3  1  9  0  2  4  6  8   &amp;lt;- original 1930 number row
  /  ,  .  P  Y  F  G  C  R  L
  A  O  E  U  I  D  H  T  N  S  &amp;lt;- home row (same as Dvorak)
  ,  Q  J  K  X  B  M  W  V  Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I decided Dvorak Classic: the 1930 original, which arranges the number row to alternate hands the same way the letter rows do, rather than the sequential 1–0 of modern Dvorak. If you're going to rebuild the layout from first principles, you may as well use the version that applied the design philosophy consistently. My typing speed collapsed for several weeks, but then climbed back. Did it meaningfully help with my pain? Modestly, but it wasn't a dead end. It was the first layer of something I hadn't finished building yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Make the Keyboard First-Class
&lt;/h3&gt;

&lt;p&gt;Fixing the layout helped. What helped more was confronting the mouse.&lt;/p&gt;

&lt;p&gt;Every mouse interaction is an interrupt: lift hand from home row, locate the mouse, shift posture to reach it, execute, return hand, re-align fingers, resume. A standard keyboard already holds the forearm in full pronation, palm flat to the desk. Every lateral reach for the mouse is angular movement on top of that, loading the forearm in an already compromised position. Once or twice is nothing. Thousands of times a day, compounded across a career, is a different problem entirely. Time at the keyboard in optimal posture is the goal; the mouse was systematically undermining it.&lt;/p&gt;

&lt;p&gt;Then my MX Master died. Perfect excuse. What I hadn't accounted for was the software. Modern software is built around the pointer: graphical interfaces assume a cursor, menus are designed to be clicked, and most applications treat the keyboard as a secondary input at best. It meant being deliberate about every piece of software in my workflow: a &lt;a href="https://itsfoss.com/tiling-window-manager/" rel="noopener noreferrer"&gt;tiling window manager&lt;/a&gt;, the terminal wherever possible, &lt;a href="https://neovim.io" rel="noopener noreferrer"&gt;NeoVim&lt;/a&gt; as my editor and vim bindings injected into everything that would accept them. For software that fundamentally doesn't support keyboard-first operation, I keep a dedicated mouse layer on my keyboard. The mouse isn't gone; it's been demoted to last resort.&lt;/p&gt;

&lt;p&gt;When the keyboard becomes your only interface, its flaws stop being background noise. Every awkward modifier reach, every useless Caps Lock, every wrist contortion you'd tolerated for years; suddenly they were the whole problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Program the Keyboard You Already Have
&lt;/h3&gt;

&lt;p&gt;Remember all those patches bolted onto the keyboard over 150 years? &lt;a href="https://github.com/kmonad/kmonad" rel="noopener noreferrer"&gt;KMonad&lt;/a&gt; is a daemon written in Haskell that intercepts keyboard events at the OS level (&lt;code&gt;/dev/input&lt;/code&gt; on Linux, with equivalent drivers on Windows and macOS) and lets you throw them out, redesigning the interface from scratch for how you actually work today, on the keyboard you already own.&lt;/p&gt;

&lt;p&gt;The change that mattered most was &lt;a href="https://precondition.github.io/home-row-mods" rel="noopener noreferrer"&gt;home row mods&lt;/a&gt;: I mapped my home row keys to dual-function. Tap &lt;code&gt;A&lt;/code&gt; and it types &lt;code&gt;a&lt;/code&gt;; hold it briefly and it becomes &lt;code&gt;Ctrl&lt;/code&gt;. The same for Shift, Alt, and Super across the other home row keys. No more stretching my pinky to the corner of the keyboard; my modifiers now live exactly where my fingers rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key    Tap     Hold
─────────────────────
 a      a      Ctrl
 s      s      Alt
 d      d      Shift
 f      f      Super
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modifiers were the first fix. The second was layers: they work like a more powerful Shift key. Hold a designated key and it transforms the whole board. I built a navigation layer that turns my right hand into the navigation interface, a symbol layer for brackets and operators, a numpad layer for numbers without leaving the home row. Key count stops mattering once you have enough layers; you only need one key per finger.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base layer:                      Navigation layer (hold Nav):
'  ,  .  P  Y  F  G  C  R  L     .  .  .  .  .  .  Hom PgU PgD End
A  O  E  U  I  D  H  T  N  S     .  .  .  .  .  .  ←   ↑   ↓   →
;  Q  J  K  X  B  M  W  V  Z     @  .  .  .  .  .  .   .   .   .

tap ; -&amp;gt; ;
hold ; -&amp;gt; nav layer
@ = finger position
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layers solved the reach problem. The imbalance was a separate problem: on a standard keyboard, each index finger covers about six keys while Caps Lock, Insert, and Scroll Lock sit there doing nothing, bolted on relics that nobody had the courage to reconsider. I silenced them, redistributed the load, and spread the work more evenly across my fingers.&lt;/p&gt;

&lt;p&gt;The last piece was the thumbs. I shifted the whole layout up a row. My home row now sits where QWERTY used to live, which means my thumbs land naturally on what were the C/V and comma/period keys, real estate I gave real responsibilities: Backspace, Enter, Delete, layer switches. My strongest digits, handling the highest-frequency actions, right where they already rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Standard:                  Shifted up one row:          Remapped:

  1 2 3 4 5                  1 2 3 4 5                   Q W E R T
   Q W E R T                  @ @ @ @ T  &amp;lt;- home row      @ @ @ @ G  &amp;lt;- home row
    @ @ @ @ G  &amp;lt;- home row     A S D F G                   Z X C V B
     Z X C V B                  Z X @ @ B                   . . @ @ .
         [     @     ]               [  spacebar  ]             [     .     ]

diagrams are from the left hand perspective
@ = finger rest position; thumb sits between keys, not directly on one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cost me nothing but time, and it genuinely helped. But it also showed me the hard limits of working with my 'standard' keyboard. The stagger is still there. Your hands still converge toward the centre. The layout is still hardware constrained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Get a Proper Ergonomic Keyboard
&lt;/h3&gt;

&lt;p&gt;If KMonad is the gateway drug, buying an ergonomic keyboard is where you start paying for the habit.&lt;/p&gt;

&lt;p&gt;I started with the &lt;a href="https://ergodox-ez.com" rel="noopener noreferrer"&gt;Ergodox EZ&lt;/a&gt;. The first day I used it, the improvement was immediate and almost embarrassing. Splitting the board and placing each half at shoulder width eliminated the forward shoulder roll and inward wrist deviation that a standard keyboard forces: the exact mechanics my PT had flagged as undermining my throw.&lt;/p&gt;

&lt;p&gt;The layout abandons the legacy row-stagger entirely: &lt;a href="https://www.howtogeek.com/70291/what-is-an-ortholinear-keyboard-and-should-you-use-one/" rel="noopener noreferrer"&gt;ortholinear&lt;/a&gt;, keys in a perfect grid, fingers moving straight up and down the way they actually extend and curl.&lt;/p&gt;

&lt;p&gt;The three main grid forms, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Row-stagger (legacy):     Ortholinear (grid form):
    Q W E R T                 Q W E R T
     A S D F G                A S D F G
      Z X C V                 Z X C V

  Column-stagger: staggered to finger length

        E            &amp;lt;- middle highest
      W D R T
    Q S C F G
    A X   V B
    Z                &amp;lt;- pinky lowest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dedicated &lt;a href="https://mattgemmell.scot/thumb-keys/" rel="noopener noreferrer"&gt;thumb cluster&lt;/a&gt; put backspace and enter exactly where KMonad had trained my thumbs to expect them, now reflected in hardware rather than software.&lt;/p&gt;

&lt;p&gt;These boards run &lt;a href="https://qmk.fm" rel="noopener noreferrer"&gt;QMK&lt;/a&gt;, open-source firmware on the keyboard's own microcontroller, so everything from KMonad moves into the board itself and travels with it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ergodox-ez.com" rel="noopener noreferrer"&gt;Ergodox EZ&lt;/a&gt; is also large and heavy. After the novelty wore off, carrying it anywhere felt like a commitment. I downsized to the &lt;a href="https://www.zsa.io/moonlander" rel="noopener noreferrer"&gt;Moonlander Mark 1&lt;/a&gt;, same company (&lt;a href="https://www.zsa.io" rel="noopener noreferrer"&gt;ZSA&lt;/a&gt;), smaller footprint, fold-out legs for &lt;a href="https://docs.splitkb.com/frequently-asked-questions/about-split-keyboards/functionality/tenting-tilting" rel="noopener noreferrer"&gt;tenting&lt;/a&gt;. Tenting rotates each half from palms-flat toward palms-facing-each-other, relieving the forearms of the all-day compression that full pronation imposes. Even a modest angle makes a noticeable difference.&lt;/p&gt;

&lt;p&gt;Then the real experimentation started. I was already building layers for navigation and symbols. I started deactivating keys I never reached: the outer columns, the number row, the thumb keys I couldn't comfortably hit. Each time I'd wait a week. If I didn't miss it, it was gone. I got the Moonlander down to around 40 active keys; every deactivated key was one less lateral stretch or over extension, before the problem became obvious: I was fighting the hardware. The board still had all those physical keys; I was just pretending they weren't there. If I was going to work with 36 keys, I should actually have 36 keys. That question had already been answered somewhere in the open-source community. I just hadn't looked yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Join the Open-Source Community
&lt;/h3&gt;

&lt;p&gt;The community exists for exactly this kind of problem: people who treat keyboard design the way athletes treat training, with repetition, obsession, and enough meticulous documentation to distinguish what actually helped from what just felt like progress. Share everything. Someone's build log from two years ago will save you weeks today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/manna-harbour/miryoku" rel="noopener noreferrer"&gt;Miryoku&lt;/a&gt; was what I found. It made the case that 36 keys, with the right layer design, covers the vast majority of what you actually do at a computer, and explained precisely why each key is placed where it is. It runs on &lt;a href="https://qmk.fm" rel="noopener noreferrer"&gt;QMK&lt;/a&gt; and &lt;a href="https://zmk.dev" rel="noopener noreferrer"&gt;ZMK&lt;/a&gt; and on &lt;a href="https://github.com/kmonad/kmonad" rel="noopener noreferrer"&gt;KMonad&lt;/a&gt;. That last point mattered: I could run the layout on my current keyboard before committing to new hardware, with the guarantee the experience would transfer.&lt;/p&gt;

&lt;p&gt;The community produces dozens of open-source designs: the &lt;a href="https://github.com/foostan/crkbd" rel="noopener noreferrer"&gt;Corne&lt;/a&gt; (a 42-key &lt;a href="https://ergotype.pro/blog/09-column-staggered-keyboards/" rel="noopener noreferrer"&gt;column-staggered&lt;/a&gt; split that became a reference design for the whole community), the &lt;a href="https://github.com/GEIGEIGEIST/TOTEM" rel="noopener noreferrer"&gt;Totem&lt;/a&gt; (38 keys, pushing minimalism&lt;br&gt;
further), and hundreds of others catalogued in the &lt;a href="https://github.com/diimdeep/awesome-split-keyboards" rel="noopener noreferrer"&gt;awesome-split-keyboards&lt;/a&gt; list on GitHub.&lt;/p&gt;

&lt;p&gt;However deep you want to go, the trade-off is always the same:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Money&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full package (board + service)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prebuilt&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kit&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electronics kit&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full DIY from open-source specs&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick what best fits your situation, not your ambition. And draw on the resources the community offers.&lt;/p&gt;

&lt;p&gt;I joined the &lt;a href="https://bastardkb.com" rel="noopener noreferrer"&gt;BastardKB&lt;/a&gt; community. I built a &lt;a href="https://github.com/Bastardkb/Charybdis" rel="noopener noreferrer"&gt;Charybdis Nano&lt;/a&gt;, a 35-key split with curved key wells and an integrated trackball on the right half, which makes the mouse layer mostly redundant, and separately the &lt;a href="https://github.com/Bastardkb/Dilemma" rel="noopener noreferrer"&gt;Dilemma&lt;/a&gt;, a smaller build with integrated trackpad, my first attempt at a portable solution. Building your own keyboard involves soldering, reading PCB datasheets, and creative problem solving. It is objectively a lot of work, but also deeply satisfying.&lt;/p&gt;

&lt;p&gt;The Charybdis Nano, for instance, has an asymmetric &lt;a href="https://mattgemmell.scot/thumb-keys/" rel="noopener noreferrer"&gt;thumb cluster&lt;/a&gt;: three keys on one side, two on the other. To keep the layout symmetric I reduced to two active thumb keys per side, the freed third key dedicated to a mouse click layer, keeping all pointer work on the right half with the trackball. That left high-frequency keys like Escape, Tab, and Caps Lock with nowhere to go. The answer came from a stenography video: steno has solved the same problem for decades, multiple keys pressed simultaneously producing a single action. That's a &lt;a href="https://docs.qmk.fm/features/combo" rel="noopener noreferrer"&gt;chord&lt;/a&gt;. &lt;a href="https://docs.qmk.fm/features/caps_word" rel="noopener noreferrer"&gt;Caps Word&lt;/a&gt; is the clearest example: both shifts together capitalises until the word ends, then turns itself off. The full layout is in the appendix.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: The Bottom of the Rabbit Hole
&lt;/h3&gt;

&lt;p&gt;By this point, the ergonomic problem is solved. The remaining problem is simpler and more stubborn: the &lt;a href="https://github.com/Bastardkb/Charybdis" rel="noopener noreferrer"&gt;Charybdis Nano&lt;/a&gt; doesn't travel. Smaller portable designs exist. The &lt;a href="https://github.com/Bastardkb/Dilemma" rel="noopener noreferrer"&gt;Dilemma&lt;/a&gt; and the &lt;a href="https://github.com/pashutk/chocofi" rel="noopener noreferrer"&gt;Chocofi&lt;/a&gt; aim specifically at portability, but they're still separate equipment: another thing to pack, unpack, and connect. The laptop is already the one device that goes everywhere. That's where the keyboard should live.&lt;/p&gt;

&lt;p&gt;The obvious answer is a keyboard that replaces the laptop keyboard entirely: not a peripheral, but the actual input layer of the machine. The catch: tie the keyboard to a laptop and a new machine means starting from scratch. That's where &lt;a href="https://frame.work/laptop16" rel="noopener noreferrer"&gt;Framework&lt;/a&gt; comes in, fully modular, open specs, right to repair, built for tinkering, with a community that documents everything.&lt;/p&gt;

&lt;p&gt;That's what I'm building. A 34-key, column-stagger board running ZMK, designed from first principles for how I actually work, replacing the laptop keyboard, thus going wherever the laptop goes. The constraints are tight, but when successful, I have a board I never have to leave behind.&lt;/p&gt;

&lt;p&gt;Look at the &lt;a href="https://github.com/foostan/crkbd" rel="noopener noreferrer"&gt;Corne&lt;/a&gt;, the &lt;a href="https://github.com/GEIGEIGEIST/TOTEM" rel="noopener noreferrer"&gt;Totem&lt;/a&gt;, the &lt;a href="https://github.com/Bastardkb/Charybdis" rel="noopener noreferrer"&gt;Charybdis&lt;/a&gt;, the hundreds of designs in the &lt;a href="https://github.com/diimdeep/awesome-split-keyboards" rel="noopener noreferrer"&gt;awesome-split-keyboards&lt;/a&gt; list; each one was almost certainly built by someone who walked this exact path and arrived at this exact conclusion. The open-source keyboard community exists because the rabbit hole reliably produces the same realisation at the bottom of it: nothing out there was built for you, so you build it yourself.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion: What I Know Now
&lt;/h2&gt;

&lt;p&gt;I've had no season-ending injuries since. I can't tell you whether it was the keyboard, the posture, or just time. I'm not sure it matters. The desk stopped working against my training, and that was the point.&lt;/p&gt;

&lt;p&gt;One honest regret: the order this article follows isn't the order I followed. I bought the &lt;a href="https://ergodox-ez.com" rel="noopener noreferrer"&gt;Ergodox&lt;/a&gt; before I'd remapped a single key, reached Step 5 before I'd properly done Step 2. The sequence here is the one that makes sense. Do as I say, not as I did.&lt;/p&gt;

&lt;p&gt;Most professionals have researched their laptop, their monitor, their chair. The keyboard is the one tool nobody questions, and the one your hands never leave. For something that touches every working hour of a career, that's a strange place to accept the default.&lt;/p&gt;

&lt;p&gt;You don't have to be an athlete for accumulated strain to matter. The season-ending injury was my wake-up call, but the underlying problem belongs to anyone who types for work: a keyboard built for a machine that stopped being manufactured a hundred years ago, accepted by most without question.&lt;/p&gt;

&lt;p&gt;You don't have to go as far as I did. A different layout, a remapped key, an afternoon with KMonad; for many people that's enough. But start somewhere. The keyboard was never designed for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layout&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://getreuer.info/posts/keyboards/alt-layouts/index.html" rel="noopener noreferrer"&gt;A guide to alt keyboard layouts&lt;/a&gt;, covers why to switch,
which layout to choose, and how the major options compare&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/manna-harbour/miryoku" rel="noopener noreferrer"&gt;Miryoku&lt;/a&gt;, the clearest documented minimal layout; explains every
decision and runs on any firmware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Software&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kmonad/kmonad" rel="noopener noreferrer"&gt;KMonad&lt;/a&gt;, remap your existing keyboard before buying anything&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qmk.fm" rel="noopener noreferrer"&gt;QMK&lt;/a&gt; and/or &lt;a href="https://zmk.dev" rel="noopener noreferrer"&gt;ZMK&lt;/a&gt;, open-source firmware for programmable hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;: &lt;a href="https://yal-tools.github.io/ergo-keyboards/" rel="noopener noreferrer"&gt;Ergo Keyboards&lt;/a&gt;, filterable list of commercial and DIY&lt;br&gt;
boards&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community&lt;/strong&gt;: &lt;a href="https://github.com/diimdeep/awesome-split-keyboards" rel="noopener noreferrer"&gt;awesome-split-keyboards&lt;/a&gt;, open-source builds and&lt;br&gt;
the people behind them&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Research&lt;/strong&gt;:&lt;br&gt;
&lt;a href="https://stacks.cdc.gov/view/cdc/191592/cdc_191592_DS1.pdf" rel="noopener noreferrer"&gt;Wrist and Forearm Posture from Typing on Split and Vertically Inclined Computer Keyboards&lt;/a&gt;,&lt;br&gt;
peer-reviewed study on how split keyboards reduce ulnar deviation and forearm&lt;br&gt;
pronation&lt;/p&gt;


&lt;h2&gt;
  
  
  My Layout
&lt;/h2&gt;

&lt;p&gt;The current layout on all my keyboards is 34 keys: five columns, three rows per finger, two thumb keys per hand (5×3+2). Diagrams are in grid form, but the layout itself is grid-agnostic: it runs on ortholinear, row-stagger, and&lt;br&gt;
column-stagger boards equally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Left half                     Right half

Base layer (Dvorak Classic):

  '    ,    .    P    Y       F    G    C    R    L
  A    O    E    U    I       D    H    T    N    S
  ;    Q    J    K    X       B    M    W    V    Z
                 _   Spc      Ent  Del


Number layer — Left:

  \    `    [    /    =       .   PrSc  Cut Copy Past
  7    5    3    1    9       .  Shift Ctrl Alt Super
  .    .    .    ;    .       .    .    .    .    .
                 -   Spc    [Ent] Del


Number layer — Right:

 Past Copy Cut PrSc   .       =    /    ]    `    \
Super Alt Ctrl Shift  .       0    2    4    6    8
  .    .    .    .    .       .    .    .    .    .
                 -  [Spc]    Ent  Del


Nav layer:

  .    .    .    .    .       .   Hom  PgU  PgD  End
Super Alt Ctrl Shift  .       .    ←    ↑    ↓    →
  .    .    .    .    .       .    .    .    .    .
                [-]  Spc     Ent  Del


Function layer:

 Reset .    .    .    .       .    .    .    .    .
 F7   F5   F3   F1   F9      F10   F2   F4   F6   F8
  .    .    .   F11   .       .   F12   .    .    .
                [-   Spc]    Ent  Del


Media layer:

 Prv  BrD  BrU  Nxt   .       .   Nxt  BrU  BrD  Prv
  .   VolD Mute VolU  .       .   VolU Mute VolD  .
  .    .   Stp  Ply   .       .   Ply  Stp   .    .
                 /   Spc    [Ent  Del]

Home Row Mods:

Super Alt Ctrl Shift .    . Shift Ctrl Alt Super

Chords:

  Space + O     →  Tab
  Space + U     →  Backspace
  Enter + H     →  Escape
  Enter + N     →  Insert
  Shift + Shift →  Caps Word

Layer access (hold):

  -              →  Nav
  Space          →  Number (right hand)
  Enter          →  Number (left hand)
  Space + -      →  Function
  Enter + Delete →  Media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






</description>
      <category>ergonomics</category>
      <category>keyboard</category>
      <category>productivity</category>
      <category>hardware</category>
    </item>
    <item>
      <title>AI-Powered Data Retrieval: A Smart Solution or Hidden Cost Trap?</title>
      <dc:creator>Huib van geertruy</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:08:15 +0000</pubDate>
      <link>https://dev.to/psgtechnautsnl/ai-powered-data-retrieval-a-smart-solution-or-hidden-cost-trap-532p</link>
      <guid>https://dev.to/psgtechnautsnl/ai-powered-data-retrieval-a-smart-solution-or-hidden-cost-trap-532p</guid>
      <description>&lt;h2&gt;
  
  
  Imagine this Scenario
&lt;/h2&gt;

&lt;p&gt;You are building a web application where you want to provide information to visitors of your website.&lt;br&gt;&lt;br&gt;
You don't have information about this subject, so you have to rely on other sources on the internet. You want the information to be as consistent and accurate as possible. You looked around but were not able to find an external API that provides the data that you need. Still, there are more than enough sources available online that are there for the taking, right? You just need something to get it for you and mould it into a format that you can consume.&lt;/p&gt;

&lt;p&gt;You could just use/build a website scraper like everyone else does. This is a popular method to get data from the web and has a proven track record. But they require knowledge about where to get the data and can be costly to maintain.&lt;/p&gt;

&lt;p&gt;So what about AI? The way AI works, we won't have to tell it where to get the data — it can just "get" it for you, right..? We won't have to worry about the structure of the data; we can just ask it to provide it in our preferred format.&lt;/p&gt;

&lt;p&gt;Let's find out! In this post, I will take you along a journey with me and try different approaches to achieve this. With every approach, we will evaluate how it worked for us. For this pet project, let's say we want to build a web application where users can search for a ski lift in a ski resort and get details about this ski lift, like the brand, capacity, etc.&lt;/p&gt;
&lt;h3&gt;
  
  
  So How Would That Work?
&lt;/h3&gt;

&lt;p&gt;Let's define two major steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use AI to find the data for us on the web.&lt;/li&gt;
&lt;li&gt;Use AI to transform the data into a suitable format that our application can consume.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Requirements &amp;amp; Criteria
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Quality&lt;/strong&gt;: The data should be consistent and accurate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: The solution should be cost-efficient; you are prepared to pay for good, reliable data, but you don't want to pay more than you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: The solution should be robust — you don't want to have to fix something whenever a data source changes or is removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: How practical is this solution?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Limitations and Disclaimers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;There are many AI models out there (and more will come), each with its strengths and weaknesses. For the sake of brevity, we will use the ChatGPT AI model in this post. Results may vary with other models, and you are free to try them out yourself.&lt;/li&gt;
&lt;li&gt;Gathering information from other websites to use in your web application can have commercial and/or ethical challenges. If you plan to do this commercially, make sure that it is legal for you to use the information that you gathered.&lt;/li&gt;
&lt;li&gt;The pieces of code provided here are kept minimal for brevity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It will be like a little adventure! We will try to solve the use case we started with. We will evaluate every step and see what we can do better. The major takeaway here is to explore options — there is no "right" or "wrong" in this article.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Using AI to Find the Data for Us on the Web
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Asking ChatGPT Chat
&lt;/h3&gt;

&lt;p&gt;Let's start simple. We want data. We'll just ask ChatGPT Chat to get the data for us! Just to see how that would work.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chatgpt.com/" rel="noopener noreferrer"&gt;ChatGPT Chat&lt;/a&gt; is a chatbox where you can talk directly with ChatGPT. It provides an answer to any query that you give it. It's really accessible and easy to use; you don't even need an account. Just start typing. It is a bit like magic — it can get everything we need, and we won't have to care where to look, right? We just need to make sure that ChatGPT gives us what we actually want.&lt;/p&gt;

&lt;p&gt;So, how would that look using the ChatGPT Chat? We would come up with a prompt something like this:&lt;/p&gt;
&lt;h4&gt;
  
  
  Prompting
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a helpful assistant.
You will give us accurate, concise information about a ski lift in a ski resort, which we will provide at the end of this prompt.
You will find these ski lift details: name, lift type, manufacturer, operator, capacity (people/hour), duration (mins), occupancy (persons), construction (year), lift_elevation distance (meters), lift_distance (meters), ski_resort, source_urls.
Provide only the data in a JSON object using the exact matching keys without markdown formatting, where source_urls should be an array of URLs.
Ski lift: Colomba in Val Cenis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Breaking It Down
&lt;/h4&gt;

&lt;p&gt;Let's break down what the prompt does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We tell ChatGPT about how it should act. This is necessary so ChatGPT knows how to best answer our query. We tell it to act as a helpful assistant and what we expect it to return. This is called "system prompting."&lt;/li&gt;
&lt;li&gt;We will give it specific details to look for. This is necessary so we know exactly the data that ChatGPT will provide, so we can consume it in our application.&lt;/li&gt;
&lt;li&gt;We tell ChatGPT to provide the data in JSON format. We are not interested in anything else. This saves resources and makes it easy for us to consume the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Result
&lt;/h4&gt;

&lt;p&gt;If we run the call, presto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Colomba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lift_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6-person high-speed detachable chairlift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manufacturer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doppelmayr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Val Cenis Ski Resort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occupancy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"construction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lift_elevation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;159&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lift_distance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ski_resort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Val Cenis – Lanslevillard/Lanslebourg/Termignon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.skidetails.com/ski-resort/val-cenis-lanslevillardlanslebourgtermignon/ski-lifts/l91126/"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we have our data! Wow, that was surprisingly easy! Let's evaluate this step.&lt;/p&gt;




&lt;h4&gt;
  
  
  Recap: ChatGPT Chat
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: The data is reasonably consistent because we can prompt for the properties we want. But it is hard to tell if it's accurate without fact-checking the data. We can ask ChatGPT Chat to provide source URLs to make this easier. It is possible that it will hallucinate parts of the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: There's a free tier, but expect to incur costs to improve the accuracy of the data and as your website grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: This setup will probably always return all the properties you need in a form that you can consume in your web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: It is not practical. There is no feasible way for your application to connect to the ChatGPT Chat. You won't be able to use this data in your website.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That was a promising start, right? It proved that ChatGPT can gather the data for us and provide it in the form that we want. But it's also pretty obvious that we can't use this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;There is only a user interface for the ChatGPT Chat; there is no interface for our web application.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's exactly why OpenAI provides a &lt;a href="https://openai.com/api/" rel="noopener noreferrer"&gt;ChatGPT API interface&lt;/a&gt;. We'll explore this in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asking ChatGPT API
&lt;/h3&gt;

&lt;p&gt;The ChatGPT API is an interface where you can interact with the ChatGPT programmatically. It accesses the same AI models and works similarly, but there are important differences. More on that later.&lt;/p&gt;

&lt;p&gt;To access it, you will &lt;em&gt;need&lt;/em&gt; an account and an API key. Also, in contrast to the Chat, it has no free tier but a pay-as-you-go model, charging you based on the number of tokens used.&lt;/p&gt;

&lt;p&gt;It's relatively straightforward to communicate with the API service. If you want to make the same call as we did with the ChatGPT Chat, it would look like this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHATGPT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`abc123`&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;CHATGPT_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.openai.com/v1/chat/completions`&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Can you give me details about this ski lift: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;liftName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in resort: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resort&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CHATGPT_API_KEY&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4.1-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are a helpful assistant.
      You will give us accurate, concise information about a ski lift in a ski resort, which we will provide at the end of this prompt.
      You will find these ski lift details: name, lift type, manufacturer, operator, capacity (people/hour), duration (mins), occupancy (persons), construction (year), lift_elevation distance (meters), lift_distance (meters), ski_resort, source_urls.
      Provide only the data in a JSON object using the exact matching keys without markdown formatting, where source_urls should be an array of URLs.`&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHATGPT_API_URL&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="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Breaking It Down
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Sending the API key in the header authenticates us to ChatGPT and ensures that ChatGPT knows who to charge for this request.&lt;/li&gt;
&lt;li&gt;In the API, we can send messages to ChatGPT with a specified role. This tells ChatGPT how to best process the input. For example, we can prime the model with "system input" by adding a property &lt;code&gt;role: system&lt;/code&gt; to the message. This is called input formatting.&lt;/li&gt;
&lt;li&gt;The ChatGPT API offers additional options like model selection, temperature, maximum token usage, and more (we will not explore these in this post, but I encourage you to try them out sometime).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When prompted, the ChatGPT API would return something like this:&lt;/p&gt;

&lt;h4&gt;
  
  
  Result
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chatcmpl-abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat.completion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1736778567&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Colomba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"lift_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chair lift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"manufacturer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Poma"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Société des Remontées Mécaniques de Val Cenis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"capacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"occupancy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"construction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"lift_elevation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"lift_distance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;950&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ski_resort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Val Cenis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source_urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"https://www.skidetails.com/ski-lifts/val-cenis/colomba/"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"logprobs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;205&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;149&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;354&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cached_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"audio_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reasoning_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"audio_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"accepted_prediction_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rejected_prediction_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system_fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fp_abc123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Investigating the Result
&lt;/h4&gt;

&lt;p&gt;So what does this tell us?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The response provides metadata about the request, such as the model used, the time of the request, a unique identifier, etc. The key objects we care about are &lt;code&gt;choices&lt;/code&gt; and &lt;code&gt;usage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;choices&lt;/code&gt; object, it returns a JSON object with the data we asked for. We can directly pass this into our application and use it however we want. Depending on your prompt, it can return one or more results.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;usage&lt;/code&gt; object, it details the cost of this request. Every request to the API uses tokens, and there is a cost associated with these tokens depending on the AI model used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's also dive a bit deeper in the api, like &lt;code&gt;how costs are calculated&lt;/code&gt; and &lt;code&gt;what models are out there&lt;/code&gt;. It greatly influences how useful your data is and what it will cost you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Breakdown of Costs
&lt;/h4&gt;

&lt;p&gt;We can break down the tokens into three types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt Tokens&lt;/strong&gt;: Represent the number of tokens in the input (the query you provide).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion Tokens&lt;/strong&gt;: Represent the number of tokens used to generate the output (the response).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total Tokens&lt;/strong&gt;: The sum of prompt and completion tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The total tokens used for this call are 354. The cost would depend on the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-4.1 Mini: $0.00032&lt;/li&gt;
&lt;li&gt;GPT-4.1: $0.0016&lt;/li&gt;
&lt;li&gt;GPT-5: $0.0017&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Prices are based on &lt;a href="https://platform.openai.com/docs/pricing?latest-pricing=standard" rel="noopener noreferrer"&gt;ChatGPT prices&lt;/a&gt; - checked Feb 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These costs add up depending on the complexity of the query and the model used. Always optimise your queries to minimise token usage. It helps a lot to keep your queries concise and straightforward, also it can save you a great deal to be really explicit on the output format. AI models generate elaborate responses unless you tell them not to. After all, this is where the people behind AI models make money..&lt;/p&gt;

&lt;h4&gt;
  
  
  Breakdown of Models
&lt;/h4&gt;

&lt;p&gt;ChatGPT offers different models that you can use. Which model is best for your use case depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reasoning capacity you need.&lt;/li&gt;
&lt;li&gt;The cost you are willing to incur.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Common ChatGPT Models
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GPT-4.1 Mini&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low to medium complexity.&lt;/li&gt;
&lt;li&gt;High-volume, low-cost tasks.&lt;/li&gt;
&lt;li&gt;Most cost-effective.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;GPT-4.1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moderate complexity.&lt;/li&gt;
&lt;li&gt;Balanced tasks (good for general use).&lt;/li&gt;
&lt;li&gt;Good balance between cost and capability.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;GPT-5&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High complexity, nuanced tasks.&lt;/li&gt;
&lt;li&gt;Advanced reasoning, planning, and problem-solving.&lt;/li&gt;
&lt;li&gt;High cost, highest accuracy.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Differences Between ChatGPT Chat and ChatGPT API
&lt;/h3&gt;

&lt;p&gt;So, that proves our case. It works a bit differently, and this time costs are involved, but it proves that we can get the same data using the ChatGPT API as when we used the ChatGPT Chat. &lt;/p&gt;

&lt;p&gt;Or did we?&lt;/p&gt;

&lt;p&gt;There is a problem with the response. Some of you may have already noticed it. If you look carefully at the results, you will see that, while the properties are the same for both data entries, the data from the &lt;code&gt;ChatGPT API&lt;/code&gt; is fundamentally different from the data that &lt;code&gt;ChatGPT Chat&lt;/code&gt; provided.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result_chatgpt_chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Colomba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6-person high-speed detachable chairlift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;manufacturer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Doppelmayr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Val Cenis Ski Resort&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;construction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_elevation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;159&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ski_resort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Val Cenis – Lanslevillard/Lanslebourg/Termignon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source_urls&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="s2"&gt;https://www.skidetails.com/ski-resort/val-cenis-lanslevillardlanslebourgtermignon/ski-lifts/l91126/&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="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result_chatgpt_api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Colomba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chair lift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;manufacturer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Poma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Société des Remontées Mécaniques de Val Cenis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;construction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_elevation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lift_distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;950&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ski_resort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Val Cenis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source_urls&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="s2"&gt;https://www.skidetails.com/ski-lifts/val-cenis/colomba/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait... what? What happened? Assuming that both use the same model, the same parameters, and the same prompt, why is the response &lt;em&gt;entirely different&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;This puzzled me at first. I just did not understand why this happened. The first hint to what is really going on here can be found when looking at the source URLs.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resort_url_chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.skidetails.com/ski-resort/val-cenis-lanslevillardlanslebourgtermignon/ski-lifts/l91126/&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;resort_url_api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.skidetails.com/ski-lifts/val-cenis/colomba/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By looking at the source URLs, we see that they use the same website as a source, but the URLs are very different in structure. Now, it's possible that the maintainer of the website has some interesting thoughts about SEO, but that is not the case here. One of these URLs is incorrect, and that is the root of our problem. A quick copy-paste to your browser will tell you quickly which url is valid. And it turns out..&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The URL that we got from the API is total bogus.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So if the URL is wrong, it's very likely that the other data is also wrong. The reason behind this is straightforward, but it &lt;em&gt;is&lt;/em&gt; easy to miss!&lt;/p&gt;




&lt;h4&gt;
  
  
  The ChatGPT API Has No Direct Access to the Internet
&lt;/h4&gt;

&lt;p&gt;Unlike the ChatGPT Chat, the API does not browse webpages by default. Any information that the ChatGPT API provides &lt;em&gt;is input&lt;/em&gt; or &lt;em&gt;is generated by the model&lt;/em&gt;. That explains why the data makes no sense at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's all generated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a really easy pitfall to get into. ChatGPT will not warn you in any way that the data might not be accurate, &lt;em&gt;even&lt;/em&gt; if we &lt;em&gt;explicitly&lt;/em&gt; ask for accurate data. If it cannot find the data, it will just generate it and state it as factual data. This is known as &lt;em&gt;hallucinating&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You can instruct ChatGPT what to do when it can't find factual data. For example, you can tell it to leave the field blank instead of making something up to fill the field. But even then, it might still happen that it returns incorrect data instead of leaving the field blank.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  Why Is the ChatGPT API Different?
&lt;/h4&gt;

&lt;p&gt;Why does the ChatGPT API not have access to the internet? It seems like a really convenient functionality to have for the ChatGPT API. We know it is possible — the ChatGPT Chat has it, after all.&lt;/p&gt;

&lt;p&gt;There are several reasons for this, but the most important one is &lt;strong&gt;cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Web browsing can be seen as a wildcard. If you are looking for something on the internet, you will get results depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How accurate your search term is.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What information is available on the web.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While ChatGPT is very capable of finding information by browsing the web, it also charges you for any information it processes. And here's the catch: how many tokens will it need to find the data you're asking for? What if it needs to parse a page with 20k+ words to extract 20 words that are relevant to you? What if it needs to process five different webpages from different websites and aggregate this information? The amount of tokens you'll be charged to get this information will stack up quickly.&lt;/p&gt;

&lt;p&gt;How many tokens are we willing to spend on a request? What if the token limit is reached? Can the code handle incomplete results? How can you guide ChatGPT on how/where to look for the information?&lt;/p&gt;

&lt;p&gt;As you can see, it is far from certain what information we can expect and at what cost if we depend on information found on the web. Also, we would have to implement contingencies when we do not get the results we want.&lt;/p&gt;




&lt;h4&gt;
  
  
  Recap: ChatGPT API
&lt;/h4&gt;

&lt;p&gt;So, where are we now?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: We can't rely on the data at all. Because the API does not have access to the internet, it will fall back to its LLM and hallucinate the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: There will be a small cost per request depending on token and model usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: This setup will still probably return all the properties you need in a form that you can consume in your web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: It is not practical. Without access to the internet, we only get hallucinated data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of blows our use case out of the water, doesn't it? This was precisely the reason we wanted to use AI: to get us the information we are looking for without all that hassle. If it can't access that information to start with, we're kind of dead in the water..&lt;/p&gt;




&lt;h3&gt;
  
  
  Asking ChatGPT API using web-search tool
&lt;/h3&gt;

&lt;p&gt;There is something for that. It's called the &lt;code&gt;web-search&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;You can tell ChatGPT API to perform a web-search whenever it needs to get real-time data. This is an explicit opt-in, so you need to configure it or ChatGPT will start guessing.. &lt;/p&gt;

&lt;p&gt;Let's dive into it a bit.&lt;/p&gt;

&lt;p&gt;The web-search tool tells the model that it can use web_search if it needs additional information. We can pass the results back into the model so it can use this as input for your original query.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Additionally, there are specific 'search-preview' AI models available, but they are very similar from using the web-search tool and have a 'probably obsolete soon' taste about them. Still you can still try a specific search-preview model if you want to.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using the web-search tool would look something like this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHATGPT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`abc123`&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;CHATGPT_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.openai.com/v1/responses`&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Can you give me details about this ski lift: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;liftName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in resort: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resort&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CHATGPT_API_KEY&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or another tool-capable model you use&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;include&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="s2"&gt;web_search_call.action.sources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;input&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;`You are a helpful assistant.
You will give us accurate, concise information about a ski lift in a ski resort, which we will provide at the end of this prompt.
You will find these ski lift details: name, lift type, manufacturer, operator, capacity (people/hour), duration (mins), occupancy (persons), construction (year), lift_elevation distance (meters), lift_distance (meters), ski_resort, source_urls.
Provide only the data in a JSON object using the exact matching keys without markdown formatting, where source_urls should be an array of URLs.`&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHATGPT_API_URL&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="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Breaking it down
&lt;/h4&gt;

&lt;p&gt;Nothing really groundbreaking really. We just added some lines to the previous prompt:&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="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="nx"&gt;include&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="s2"&gt;web_search_call.action.sources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Result
&lt;/h4&gt;

&lt;p&gt;Running this call would give us a response like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resp_abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1736778567&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web_search_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ws_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Colomba chairlift Val Cenis manufacturer capacity length vertical rise year"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.skiresort.info/ski-resort/val-cenis-lanslevillard-lanslebourg-termignon/ski-lifts/l91126/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Colomba - Val Cenis lift details"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.skiresort.info/ski-resort/val-cenis-lanslevillard-lanslebourg-termignon/ski-lifts/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ski lifts Val Cenis – list"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.yumpu.com/en/document/view/56526718/doppelmayr-garaventa-annual-brochure-2011"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doppelmayr/Garaventa Annual Brochure 2011 (La Colomba)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Colomba&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lift_type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;6pers. High speed chairlift (detachable)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;manufacturer&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Doppelmayr&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;operator&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:null,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;capacity&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:2400,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;duration&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:4,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;occupancy&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:6,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;construction&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:2010,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lift_elevation&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:159,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lift_distance&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1051,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ski_resort&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Val Cenis&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;source_urls&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://www.skiresort.info/ski-resort/val-cenis-lanslevillard-lanslebourg-termignon/ski-lifts/l91126/&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://www.skiresort.info/ski-resort/val-cenis-lanslevillard-lanslebourg-termignon/ski-lifts/&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://www.yumpu.com/en/document/view/56526718/doppelmayr-garaventa-annual-brochure-2011&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which also is pretty similar to the previous response. You will notice the new 'action' object, which summarizes a web-search call. This will tell you the query ChatGPT executed and the sources that the query yielded. &lt;/p&gt;

&lt;p&gt;Using the web-search api tool is not free - expect to spend about $0.01 for every web-search call done + the content tokens that are put into your original query. It is hard to say how much content tokens are use because the result is non-deterministic, expect additional tokens on a typical web search - amount varies by query and sources.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recap: ChatGPT API + Web-search
&lt;/h4&gt;

&lt;p&gt;So, how did the web-search tool do?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: We &lt;em&gt;probably&lt;/em&gt; get all the results we asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: There will be a small cost involved every time the web-search tool is invoked along with content tokens from the search result. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: This setup will probably work, but we have limited control on what we query for and what results we get. We can never be sure where it gets its data from and how accurate it is. We could have conflicting or incomplete data. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: It is practical (yay!). It can get us what we need, at a minor price and the trade-off of some control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Some thoughts about control
&lt;/h4&gt;

&lt;p&gt;Like previously stated, you have limited control on what the model will look for and what results to pick. We don't even know what search engine or reasoning is behind it as this is not provided and can change over time. Also, you won't know if the results are copyrighted (so it might be a legal liability)&lt;/p&gt;

&lt;p&gt;It's a bit like a genie that fulfils all your wishes. Your wish is fulfilled, but is it what you &lt;em&gt;truly&lt;/em&gt; wanted...?&lt;/p&gt;

&lt;p&gt;Still, there are some things you can do to level things a bit.&lt;/p&gt;

&lt;h5&gt;
  
  
  Tool-choice
&lt;/h5&gt;

&lt;p&gt;The model will only use web-search whenever it &lt;em&gt;thinks&lt;/em&gt; it will need to. You can use &lt;code&gt;tool_choice: required&lt;/code&gt; to force the model to always do a search query if you need to.&lt;/p&gt;

&lt;h5&gt;
  
  
  Prompting
&lt;/h5&gt;

&lt;p&gt;You can not directly provide a search query or search options like a regular search query, but you can update your prompt and provide some direction. For example,&lt;/p&gt;

&lt;p&gt;If you want it to look for a specific query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When performing web search, use the following query exactly:
"Colomba chairlift Val Cenis capacity manufacturer length vertical year"

Do not broaden or rephrase the query.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want it to look at specific trusted (or legal) places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rules:
- You may extract data only from these domains:
  - doppelmayr.com
  - leitner.com
  - skiresort.info
- If information is not explicitly stated on those domains, return null.
- Do not infer, estimate, or merge values.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Using a Search API + ChatGPT
&lt;/h2&gt;

&lt;p&gt;So we were able to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use AI to find the data for us on the web.&lt;/li&gt;
&lt;li&gt;Use AI to transform the data into a suitable format that our application can consume.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, there are some trade-offs. We have only limited control on the web-search and its results. We can not set engine, exact query, pagination, domain filters, etc. We don't even know whether the data we get is copyrighted. It might be necessary to have more control.&lt;/p&gt;

&lt;p&gt;So let's explore that a bit: What if we want to have more control on where the data comes from? There's a straightforward answer for this. We could do what we always did:&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Search Engines and feed the result into ChatGPT
&lt;/h3&gt;

&lt;p&gt;Search engines are excellent at finding results. But results can be influenced by companies paying for visibility or may not contain all the data you need. It might also be copyrighted. To get legal, accurate and complete data, it's helpful to identify a reliable source that provides all the necessary information. You probably don't want to deal with collecting results from multiple sources to keep things simple and straightforward.&lt;/p&gt;




&lt;h4&gt;
  
  
  Getting the Data
&lt;/h4&gt;

&lt;p&gt;For this post, we'll use the &lt;code&gt;Brave Search API&lt;/code&gt;. Other search engines also offer API versions. You'll need to create an account and generate an API key to communicate with the search API. Most APIs offer a free plan with rate limits, with paid plans available if you need more traffic.&lt;/p&gt;

&lt;p&gt;Search engines &lt;em&gt;need&lt;/em&gt; accurate input. If you provide a vague prompt, you'll get vague results. It's important to carefully craft the prompt you give to the search API.&lt;/p&gt;

&lt;p&gt;Let's tell the API to prioritize results from a trusted ski data website, &lt;code&gt;https://my-trusty-ski-data-website.com&lt;/code&gt;, for best results.&lt;/p&gt;

&lt;p&gt;Here’s how it might look:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;website&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://my-trusty-ski-data-website.com&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;resort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;val cenis&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;liftName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;colomba&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;SEARCH_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`abc123`&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;SEARCH_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.search.brave.com/res/v1/web/search`&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;SEARCH_API_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// We are only interested in the first result&lt;/span&gt;
  &lt;span class="na"&gt;result_filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// We only want web results&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;headers&lt;/span&gt; &lt;span class="o"&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="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-subscription-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SEARCH_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept-Encoding&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="s2"&gt;gzip&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;GET&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="c1"&gt;// encode SEARCH_API_OPTIONS so that we can use them as a querystring&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SEARCH_API_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SEARCH_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?q=site:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%20details%20&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;resort&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%20&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;liftName&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;queryString&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="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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Breaking It Down
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;It makes a call to the Brave Search API to get results related to our query.&lt;/li&gt;
&lt;li&gt;Provides headers to include the API key and specify the result format.&lt;/li&gt;
&lt;li&gt;Configures the API to return only a single web result.&lt;/li&gt;
&lt;li&gt;Specifies the website, ski resort, and lift name in the query.&lt;/li&gt;
&lt;li&gt;Ensures the data is properly URL-encoded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This query will return one URL with (hopefully) all the information we need! However, there's no guarantee that the result will be useful. It's always a good idea to tweak the search request to make it as reliable as possible.&lt;/p&gt;




&lt;h4&gt;
  
  
  Processing the Data
&lt;/h4&gt;

&lt;p&gt;Once we access the URL, we get an entire webpage. While it should contain the data we need, it will also include unnecessary elements like headers, sidebars, footers, scripts, and styles. We need to extract the relevant data. Let’s see how this would look using ChatGPT.&lt;/p&gt;

&lt;h5&gt;
  
  
  Cost of Processing the Data
&lt;/h5&gt;

&lt;p&gt;ChatGPT is excellent at processing data, but it consumes tokens for the input you provide. So how much data are we talking about, and what would it cost?&lt;/p&gt;

&lt;p&gt;Let’s work with some numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The average HTML size of a webpage is 50–100 KB, excluding assets like media, CSS, and JavaScript. Let’s assume 100 KB for this calculation.&lt;/li&gt;
&lt;li&gt;100 KB equals 102,400 bytes.&lt;/li&gt;
&lt;li&gt;1 byte roughly equals 1 character (approximation; UTF-8 may use multiple bytes per character).&lt;/li&gt;
&lt;li&gt;1 ChatGPT token equals ~4 characters in English (or HTML-like text).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means we would need 102,400 / 4 = 25,600 tokens to feed the page into ChatGPT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price Estimate (input tokens only):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-4.1-mini: $0.01&lt;/li&gt;
&lt;li&gt;GPT-4.1: $0.051&lt;/li&gt;
&lt;li&gt;GPT-5: $0.032&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That actually doesn't sound too bad.. but it will add up quickly when numbers add up. For example, for 1000 calls a day you would be looking at $10-50.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recap: Brave API + ChatGPT API
&lt;/h4&gt;

&lt;p&gt;So, what did this bring us?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: We should be able to get pretty good data, as we can tell what we want to look for and where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: The costs can potentially be higher than we would like to as we feed the entire web result to ChatGPT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: This setup will probably return all the properties you need in a form that you can consume in your web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: It is reasonably practical but does not feel very efficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we can use ChatGPT to extract the data we need from the website source code we found, but it's not really efficient yet. Also, there is only so much data that ChatGPT can process at a time, because of something called the &lt;em&gt;context window&lt;/em&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Context Window
&lt;/h4&gt;

&lt;p&gt;Keep in mind that AI models have a context window limit. The ChatGPT API is stateless, meaning it won't retain input between API calls. There are ways to work around this (e.g., resending the input with every query or choosing a model with a larger context window), but it's clear that making your input as concise as possible before providing it to ChatGPT is crucial.&lt;/p&gt;

&lt;p&gt;Sending an entire page source into the ChatGPT API would not be my definition of concise.&lt;/p&gt;




&lt;h4&gt;
  
  
  Optimising Input Size
&lt;/h4&gt;

&lt;p&gt;What can we do to make the input smaller and more concise? A typical HTML page contains a lot of unnecessary information for our use case, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commented code&lt;/li&gt;
&lt;li&gt;HTML markup&lt;/li&gt;
&lt;li&gt;Inline styles&lt;/li&gt;
&lt;li&gt;Inline JavaScript&lt;/li&gt;
&lt;li&gt;Assets&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Use Search Engine + Scraper and feed the result into ChatGPT
&lt;/h3&gt;

&lt;p&gt;Let’s clean that up a bit!&lt;/p&gt;

&lt;p&gt;We can use a scraper for this. Scrapers are excellent at collecting data from webpages. They are typically used to extract specific data, like prices, from webpages. However, relying on specific page structures can be unreliable, as any changes to the webpage by its maintainer can break the scraper.&lt;/p&gt;

&lt;p&gt;There are two main approaches we can take:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the exact data we need using CSS selectors.&lt;/li&gt;
&lt;li&gt;Remove everything we don't need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this case, I would choose the second option. It’s a more robust and reliable way to reduce the size of our input without risking changes in the input breaking our scraper. &lt;/p&gt;

&lt;p&gt;For this post, we’ll use Cheerio, but there are many other options available.&lt;/p&gt;

&lt;p&gt;Here’s how it might look:&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheerio&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;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://url-with-data`&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;$&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;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script, style&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Remove all scripts and styles&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Get all text from the page&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawText&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;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Remove unnecessary whitespaces&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Breaking It Down
&lt;/h4&gt;

&lt;p&gt;This will give us all the text on the webpage, stripped of unnecessary elements. This approach can reduce the text size by 90% or more. For example, a 100 KB webpage would be reduced to 10 KB of text.&lt;/p&gt;




&lt;h4&gt;
  
  
  Cost Comparison
&lt;/h4&gt;

&lt;p&gt;Let’s compare the costs of processing the reduced input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPT-4.1-mini&lt;/strong&gt;: $0.001&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-4.1&lt;/strong&gt;: $0.0051&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5&lt;/strong&gt;: $0.0032&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so we can save 90% in cost, just by removing things we do not need.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recap: Brave Search + Cheerio + ChatGPT API
&lt;/h4&gt;

&lt;p&gt;So, what did this bring us?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: We should be able to get pretty good data, as we can tell what we want to look for and where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: The costs can be optimised so we don't pay more than we should or want to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: This setup will probably return all the properties you need in a form that you can consume in your web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practicality&lt;/strong&gt;: It is reasonably practical, but is the extra hassle worth it..?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, you might realise that we are taking more and more tasks out of ChatGPT’s hands. It feels like we’re falling back more and more on common, widely used practices. It sounds like we are building a regular scraper. With some AI sprinkles. &lt;/p&gt;

&lt;h4&gt;
  
  
  Why not just use the web-search tool?
&lt;/h4&gt;

&lt;p&gt;So why should we bother with all this and not just use the web-search tool? &lt;/p&gt;

&lt;p&gt;It depends. Like stated before, it all comes down to how much control you need, and the price you are willing to pay. Looking at the numbers above, the web-search API is pretty pricey compared to the methods discussed - it will add $0.01  to every call you do regardless of the other tokens used, even for small inputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: What Does This Mean?
&lt;/h2&gt;

&lt;p&gt;Here's what I take away from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Can Retrieve Web Data Directly&lt;/strong&gt;: With the &lt;code&gt;web-search&lt;/code&gt; tool the ChatGPT API can have access to the internet, at some extra cost and less control over the result. There are some things you can do to fine-tune the results up to a certain point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Excels at Data Transformation&lt;/strong&gt;: Use AI to process and structure raw data into a usable format for your application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimise Data to Save Costs&lt;/strong&gt;: Always optimise input/output size to reduce AI processing costs and improve efficiency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Has Limitations&lt;/strong&gt;: Remain mindful of constraints like the context window and potential inaccuracies (hallucinations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Is a Tool, Not a Solution&lt;/strong&gt;: The more concise/specific your query, the better your result will be. You still need to think it through to prevent getting something you don't want or can't use. Optionally combine AI with traditional methods for a more cost-effective and robust data pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: I did not discuss caching in this post, but of course it's a no-brainer to have a caching strategy whenever you work with cacheable data to optimise your code and prevent unnecessary costs.&lt;/li&gt;
&lt;/ul&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;ChatGPT Chat&lt;/th&gt;
&lt;th&gt;ChatGPT API&lt;/th&gt;
&lt;th&gt;ChatGPT API web-search&lt;/th&gt;
&lt;th&gt;Brave Search + ChatGPT API&lt;/th&gt;
&lt;th&gt;Brave Search + Cheerio + ChatGPT API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legal risk&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practicality&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Note: “Legal risk” here means the risk of ingesting/reproducing third-party web content without clear permission or provenance. Pure hallucinations may reduce copyright risk, but increase misinformation/product liability risk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Other Considerations
&lt;/h3&gt;

&lt;p&gt;AI is evolving rapidly, with new possibilities and opportunities emerging every day. The conclusions I’ve drawn here might change in the near future — perhaps even today.&lt;/p&gt;

&lt;p&gt;There are other options worth exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agentic AI&lt;/strong&gt;: Agentic AI is like you order a person to do a job for you, like 'fetch me the cheapest flight to Annecy', or 'do all my declarations for this month'. You could also ask it to gather information for you, like we did in this post. This will probably do the job, but it might take time to yield results and will be hard to scale. It also sounds a bit like using a sledgehammer to drive in a nail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting/training your AI&lt;/strong&gt;: It gives you more control on your costs, but purchasing or using your own hardware might not necessarily be cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using a paid solution like roborabbit.com&lt;/strong&gt;: This could be a viable alternative depending on your needs.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
