<?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: Dinesh Wijethunga</title>
    <description>The latest articles on DEV Community by Dinesh Wijethunga (@dineshstack).</description>
    <link>https://dev.to/dineshstack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3550573%2F24b18188-f648-4ede-864e-3b977482ced0.jpg</url>
      <title>DEV Community: Dinesh Wijethunga</title>
      <link>https://dev.to/dineshstack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dineshstack"/>
    <language>en</language>
    <item>
      <title>5 Laravel Migration Mistakes That Made It Into Production (and How to Repair Them)</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Wed, 05 Aug 2026 16:10:11 +0000</pubDate>
      <link>https://dev.to/dineshstack/5-laravel-migration-mistakes-that-made-it-into-production-and-how-to-repair-them-c3g</link>
      <guid>https://dev.to/dineshstack/5-laravel-migration-mistakes-that-made-it-into-production-and-how-to-repair-them-c3g</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A migration I shipped created a column literally named &lt;code&gt;36&lt;/code&gt;. It is still in production. Here are five migration mistakes from a live Laravel e-commerce store, what each one actually did to the schema, and how to repair them without downtime.&lt;/p&gt;
&lt;p&gt;Every schema article you have read shows you the clean version — including one of mine. The tables are tidy, the indexes are deliberate, and the author implies they got it right the first time.&lt;/p&gt;
&lt;p&gt;Here is the other half. Seven weeks after that schema went live, the cart tables had picked up four follow-up migrations, one silent bug that is still sitting in production today, and a repair migration that quietly contradicts the one before it. None of this is hypothetical: I pulled the column list off the live database while writing this.&lt;/p&gt;
&lt;h2&gt;Mistake #1: calling char() without a column name&lt;/h2&gt;
&lt;p&gt;This is the one that still makes me wince. The migration was named &lt;code&gt;add_oId_selected_user_id_to_cart_items_table&lt;/code&gt;. The intent was obvious — add a 36-character UUID column called &lt;code&gt;oId&lt;/code&gt;. This is what shipped:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Schema::table('cart_items', function (Blueprint $table) {
    $table-&amp;gt;foreignId('user_id')-&amp;gt;nullable()-&amp;gt;constrained();
    $table-&amp;gt;char(36)-&amp;gt;nullable();          // &amp;lt;-- the bug
    $table-&amp;gt;json('selected')-&amp;gt;nullable();
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why it is wrong&lt;/h3&gt;
&lt;p&gt;Laravel's signature is &lt;code&gt;char(string $column, ?int $length = null)&lt;/code&gt;. The first argument is the column name, not the length. Passing &lt;code&gt;36&lt;/code&gt; makes it the name — PHP happily coerces the integer &lt;code&gt;36&lt;/code&gt; to the string &lt;code&gt;"36"&lt;/code&gt;, no error, no warning, migration exits green.&lt;/p&gt;
&lt;p&gt;So instead of a 36-character column named &lt;code&gt;oId&lt;/code&gt;, the database got a column named &lt;code&gt;36&lt;/code&gt; at the default length of 255. Here it is on the live server right now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SHOW COLUMNS FROM cart_items;

Field                | Type            | Null | Key
---------------------+-----------------+------+-----
row_id               | varchar(255)    | NO   | MUL
oId                  | varchar(255)    | YES  |
...
36                   | char(255)       | YES  |
selected             | json            | YES  |&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;oId&lt;/code&gt; exists too — added six weeks later by a different migration, as a &lt;code&gt;varchar(255)&lt;/code&gt;. So the table carries both the mistake and its replacement, and the replacement is not even the type that was originally intended.&lt;/p&gt;
&lt;p&gt;The migration ran green because there is nothing invalid about a column named &lt;code&gt;36&lt;/code&gt;. MySQL allows it. Every test passed. Nobody noticed for six weeks.&lt;/p&gt;
&lt;h3&gt;The correct way&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Schema::table('cart_items', function (Blueprint $table) {
    $table-&amp;gt;char('oId', 36)-&amp;gt;nullable()-&amp;gt;after('row_id');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And to clean up after the fact — note that a numeric column name has to be quoted in raw SQL, though Laravel's builder handles it for you:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Schema::table('cart_items', function (Blueprint $table) {
    if (Schema::hasColumn('cart_items', '36')) {
        $table-&amp;gt;dropColumn('36');
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check it is empty before you drop it. On this store it is entirely NULL, because nothing ever wrote to a column no code knew existed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT COUNT(*) AS non_null FROM cart_items WHERE `36` IS NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a final twist. When I ran that check while writing this, &lt;code&gt;36&lt;/code&gt; came back with zero non-null rows — expected, since no code ever referenced it. But so did &lt;code&gt;oId&lt;/code&gt;. The replacement column is empty too. Six weeks of migration churn, a bug that reached production, and a repair migration to fix it, all for a feature that was never finished. That is worth sitting with: the cost of a sloppy migration is not only the bad column, it is every migration you write afterwards to work around it.&lt;/p&gt;
&lt;h2&gt;Mistake #2: promising the schema is final&lt;/h2&gt;
&lt;p&gt;My original write-up of this schema ended with a confident claim: get the composite indexes and nullable columns right on day one, and you will never need a painful migration during a peak-traffic sale.&lt;/p&gt;
&lt;p&gt;The migration log disagrees. Between 12 November and 29 December:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Migration&lt;/th&gt;
&lt;th&gt;What it added&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nov 12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;add_order_id_status_to_cart_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nov 16&lt;/td&gt;
&lt;td&gt;&lt;code&gt;add_oId_selected_user_id_to_cart_items_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;36&lt;/code&gt;, &lt;code&gt;selected&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 29&lt;/td&gt;
&lt;td&gt;&lt;code&gt;add_user_columns_to_cart_items_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;selected&lt;/code&gt;, &lt;code&gt;oId&lt;/code&gt; (both guarded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 29&lt;/td&gt;
&lt;td&gt;&lt;code&gt;add_user_shipping_method_columns_to_carts_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shop_shipping_method_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;Why it is wrong&lt;/h3&gt;
&lt;p&gt;Not because the schema was careless — the four core tables and their composite uniques have never changed. It is wrong because you cannot know your query patterns before the product has users. &lt;code&gt;status&lt;/code&gt; only became obviously necessary once someone asked for a report grouping carts by active, converted, abandoned, and expired. Nobody thinks of that on day one.&lt;/p&gt;
&lt;h3&gt;The correct way&lt;/h3&gt;
&lt;p&gt;Design for additive change instead of claiming finality. Concretely: add a &lt;code&gt;status&lt;/code&gt; column from the start even if it only ever holds one value, because every reporting query eventually wants one and backfilling it later means inferring history you no longer have. Keep new columns nullable so the migration never locks a large table rewriting rows. And treat "we needed another column" as normal, not as failure.&lt;/p&gt;
&lt;h2&gt;Mistake #3: commenting out a block instead of deleting it&lt;/h2&gt;
&lt;p&gt;The December repair migration shipped like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Schema::table('cart_items', function (Blueprint $table) {
    // User support
//  $table-&amp;gt;foreignId('user_id')
//      -&amp;gt;nullable()
//      -&amp;gt;after('cart_id')
//      -&amp;gt;constrained()
//      -&amp;gt;cascadeOnDelete();

    if (!Schema::hasColumn('cart_items', 'selected')) {
        $table-&amp;gt;json('selected')-&amp;gt;nullable()-&amp;gt;after('options');
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why it is wrong&lt;/h3&gt;
&lt;p&gt;A commented-out block carries no information. Six months later nobody can tell whether it means "not yet", "already handled by the November migration", or "this broke production and we backed it out". All three are plausible readings of the same five lines, and they imply completely different next actions.&lt;/p&gt;
&lt;p&gt;Git already stores the history. The commented code is not a record, it is ambiguity.&lt;/p&gt;
&lt;h3&gt;The correct way&lt;/h3&gt;
&lt;p&gt;Delete it. If the reason matters, say it in one line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Schema::table('cart_items', function (Blueprint $table) {
    // user_id was already added by the 2025_11_16 migration.
    if (!Schema::hasColumn('cart_items', 'selected')) {
        $table-&amp;gt;json('selected')-&amp;gt;nullable()-&amp;gt;after('options');
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mistake #4: hasColumn() guards used to paper over drift&lt;/h2&gt;
&lt;p&gt;Look at that December migration again. It adds &lt;code&gt;selected&lt;/code&gt; — a column the November migration already added — wrapped in a &lt;code&gt;Schema::hasColumn()&lt;/code&gt; check. Same story for &lt;code&gt;oId&lt;/code&gt;, and for &lt;code&gt;shop_shipping_method_id&lt;/code&gt; on the &lt;code&gt;carts&lt;/code&gt; table.&lt;/p&gt;
&lt;h3&gt;Why it is wrong&lt;/h3&gt;
&lt;p&gt;To be fair to the pattern: these guards are legitimate in real situations — squashed migration histories, per-tenant schemas that genuinely diverge, or a shared package migration that may or may not have run. That is not what happened here.&lt;/p&gt;
&lt;p&gt;Here they appear because the author was not sure whether the November migration had fully applied. The guard makes the migration safe to re-run, which is good, but it also makes environment drift invisible, which is the actual problem. A migration that works whether or not the previous one ran is a migration that has stopped telling you the truth about your schema.&lt;/p&gt;
&lt;p&gt;The tell is that the guarded &lt;code&gt;oId&lt;/code&gt; was added as &lt;code&gt;string&lt;/code&gt; (varchar 255) while the November attempt was &lt;code&gt;char&lt;/code&gt;. Two different intentions for the same column, six weeks apart, neither aware of the other.&lt;/p&gt;
&lt;h3&gt;The correct way&lt;/h3&gt;
&lt;p&gt;Find out what actually ran before writing the repair:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php artisan migrate:status&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then write a migration that states one intention plainly, and reserve &lt;code&gt;hasColumn()&lt;/code&gt; for schema that is genuinely conditional rather than merely uncertain.&lt;/p&gt;
&lt;h2&gt;Mistake #5: repairing with the wrong type&lt;/h2&gt;
&lt;p&gt;The column that finally landed is &lt;code&gt;oId varchar(255)&lt;/code&gt;. The name and the original &lt;code&gt;char(36)&lt;/code&gt; attempt both say the same thing: this holds a UUID.&lt;/p&gt;
&lt;h3&gt;Why it is wrong&lt;/h3&gt;
&lt;p&gt;A UUID string is always exactly 36 characters. Declaring it &lt;code&gt;varchar(255)&lt;/code&gt; gives up a fixed-width guarantee for nothing, and if the column is ever indexed the index entry is sized for the declared maximum, not the actual data. On a hot table like &lt;code&gt;cart_items&lt;/code&gt; that is real cost for zero benefit.&lt;/p&gt;
&lt;h3&gt;The correct way&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// A UUID is fixed-width. Say so.
$table-&amp;gt;char('oId', 36)-&amp;gt;nullable();

// Or, if you control the write path and want it compact:
$table-&amp;gt;uuid('oId')-&amp;gt;nullable();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Changing an existing column's type needs &lt;code&gt;doctrine/dbal&lt;/code&gt; on older Laravel versions, and on a large table it is a rewrite — do it in a maintenance window or with an online schema change tool, not casually during a sale.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Four of these five are the same underlying failure: a migration that ran successfully while doing the wrong thing. &lt;code&gt;char(36)&lt;/code&gt; exited green. The guarded re-adds exited green. The commented block exited green. Nothing in CI can catch any of them, because nothing about them is invalid — they are merely wrong.&lt;/p&gt;
&lt;p&gt;The habits that would have caught them are unglamorous:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the generated SQL, not just the migration.&lt;/strong&gt; &lt;code&gt;php artisan migrate --pretend&lt;/code&gt; would have shown &lt;code&gt;add `36` char(255)&lt;/code&gt; in about one second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff the live schema against what you think you have.&lt;/strong&gt; A &lt;code&gt;SHOW COLUMNS&lt;/code&gt; in code review is cheaper than a six-week-old mystery column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat a &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;hasColumn()&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; guard as a question, not an answer.&lt;/strong&gt; If you needed one, find out why first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume you will need more columns.&lt;/strong&gt; Additive change is normal; pretending otherwise is what makes it painful.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The schema itself held up fine — the four tables and both composite uniques are untouched since launch, and I would design them the same way again. It was the follow-up migrations, the ones nobody reviews as carefully, that did the damage.&lt;/p&gt;
&lt;p&gt;If you want the schema those migrations were built on, I wrote it up in full: &lt;a href="https://dineshstack.com/en/shopping-cart-database-schema-design-laravel-ecommerce" rel="noopener noreferrer"&gt;the complete shopping cart database design&lt;/a&gt;, including the two composite unique constraints that turned out to be the only decisions I did not have to revisit.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Cinematic AI Video Prompts: A Step-by-Step Formula</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Tue, 04 Aug 2026 20:33:57 +0000</pubDate>
      <link>https://dev.to/dineshstack/cinematic-ai-video-prompts-a-step-by-step-formula-4nfc</link>
      <guid>https://dev.to/dineshstack/cinematic-ai-video-prompts-a-step-by-step-formula-4nfc</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Amateur AI clips come from underspecified prompts. Name the camera move, the lens, the light and the grade, and the model stops guessing. The order that works: subject, setting, action, camera, lens, lighting, mood, film-look, then the technical flags. Keep every prompt free of logos and readable screens — that one rule improves quality and makes the footage reusable.&lt;/p&gt;
&lt;p&gt;This is post #3 of the &lt;a href="https://dineshstack.com/en/automate-ai-video-creation-claude-higgsfield" rel="noopener noreferrer"&gt;AI Video Automation series&lt;/a&gt;. The prompts below are the ones that go in the &lt;code&gt;prompt&lt;/code&gt; column of the shot list the &lt;a href="https://dineshstack.com/en/higgsfield-python-batch-queue-runner" rel="noopener noreferrer"&gt;batch runner&lt;/a&gt; walks.&lt;/p&gt;
&lt;h2&gt;Why most AI clips look amateur&lt;/h2&gt;
&lt;p&gt;Almost always the same cause: the prompt names a subject and stops. "A mechanic in a workshop." The model then has to invent the camera position, the movement, the lens, the light and the colour — and it invents all five badly, because there is no signal telling it what to do.&lt;/p&gt;
&lt;p&gt;A cinematic prompt removes the guessing by stating what a cinematographer would decide before rolling. That is the entire technique. Everything below is detail on how to say it.&lt;/p&gt;
&lt;h2&gt;The prompt anatomy&lt;/h2&gt;
&lt;p&gt;Write the elements in this order. Models weight earlier tokens more heavily, so subject and setting lead, and technical flags trail:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[subject] + [setting] + [action] +&lt;br&gt;
[camera movement] + [lens/framing] +&lt;br&gt;
[lighting] + [mood] + [film-look] +&lt;br&gt;
[technical: aspect ratio, duration, motion intensity, seed]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A complete example that renders cleanly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A confident mechanic in clean overalls in a workshop, arms crossed,&lt;br&gt;
slight smile, looking just off-camera — slow push-in, 50mm, shallow&lt;br&gt;
depth of field — soft directional window light — calm, assured mood —&lt;br&gt;
cinematic teal-and-amber grade, photorealistic — 9:16, 5s, motion: low&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a real establishing shot from a production b-roll bank, following the same order:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Wide interior of a busy modern auto workshop, two cars raised on&lt;br&gt;
hydraulic lifts, tools neatly arranged, shafts of warm afternoon&lt;br&gt;
sunlight cutting through a large roller door, faint dust in the air,&lt;br&gt;
slow steady dolly-in, 35mm, shallow depth of field, cinematic&lt;br&gt;
teal-and-amber grade, photorealistic, motion: low&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note how much of that prompt is not the subject. "Shafts of warm afternoon sunlight," "faint dust in the air," "slow steady dolly-in" — those clauses are doing the cinematic work.&lt;/p&gt;
&lt;h2&gt;The load-bearing lines&lt;/h2&gt;
&lt;p&gt;If you only add four things to a bare prompt, add these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Camera movement&lt;/strong&gt; — "slow push-in", "gentle handheld drift", "steady dolly-in". This single line is the biggest quality jump available. A static prompt produces a static, lifeless clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighting&lt;/strong&gt; — "warm afternoon light through a window", "soft directional light", "golden hour". Light is what actually reads as cinematic; grade alone does not fake it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lens and framing&lt;/strong&gt; — "35mm, shallow depth of field", "macro close-up". Gives depth and tells the model where to put focus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Film-look&lt;/strong&gt; — "cinematic teal-and-amber grade, photorealistic". Anchors both colour and realism, and stops the output drifting toward illustration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then one dial: &lt;strong&gt;motion intensity&lt;/strong&gt;. Keep it &lt;code&gt;low&lt;/code&gt; for people and subtle shots, &lt;code&gt;medium&lt;/code&gt; for action. High motion on a human face warps it — this is the single most reliable way to ruin an otherwise good clip.&lt;/p&gt;
&lt;h2&gt;Model selection is part of the prompt&lt;/h2&gt;
&lt;p&gt;Which model you send a shot to matters as much as the wording, and the two decisions are linked.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kling 3.0&lt;/strong&gt; — strongest for realistic people and controlled camera moves. Use it for hero shots. Keep the action to one clear beat; multi-action prompts wobble in a five-second clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seedance 2.0&lt;/strong&gt; — faster and cheaper, excellent for abstract motion, gradients and background plates. Reserve it for non-hero shots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration&lt;/strong&gt; — 5–6 second clips render more reliably and cost fewer credits than long ones. You cut them down in the edit anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That advice is not theoretical. Across a real 57-shot library the split came out as:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Shots&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;Hero shots — people, controlled moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 2.0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Background plates, abstract motion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nano Banana Pro&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Stills where a still will do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Voiceover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Roughly a 3:1 ratio of hero to background. Sending those 12 background plates to Kling instead would have changed nothing visible and cost noticeably more — the kind of decision that only shows up when you price the library before running it.&lt;/p&gt;
&lt;h2&gt;The brand-neutral rule&lt;/h2&gt;
&lt;p&gt;Keep every prompt free of logos, product names and readable screens. This is worth stating as a rule because it wins twice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It improves quality.&lt;/strong&gt; Current video models render text as garbled pseudo-lettering. Ask for a screen with a UI and you get something that looks broken. So write the screen out of the shot instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Close-up of a technician's hands holding a rugged tablet in a workshop,&lt;br&gt;
tapping and swiping, screen deliberately out of focus / glare so no UI&lt;br&gt;
is readable, grease-flecked fingers, soft window light, macro, shallow&lt;br&gt;
depth of field, cinematic, photorealistic, motion: low&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;"Screen deliberately out of focus / glare so no UI is readable" is doing real work there. You composite the actual interface in the edit, where it renders perfectly because it is a real screenshot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It makes the footage reusable.&lt;/strong&gt; A clip with no branding in it can serve any project. A generated library built this way keeps its value after the campaign it was made for is finished — which changes the economics of generating it at all.&lt;/p&gt;
&lt;h2&gt;Common mistakes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No camera or lighting direction.&lt;/strong&gt; The single biggest cause of flat, amateur output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable text or logos.&lt;/strong&gt; Models render text as garbled shapes. Composite it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too much action in five seconds.&lt;/strong&gt; One clear beat per short clip; chain beats in the edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generating once.&lt;/strong&gt; Run 3–5 seeds, keep the best, discard the rest. First takes are rarely the best takes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High motion on faces.&lt;/strong&gt; Warping is almost guaranteed. Drop to &lt;code&gt;low&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requesting on-screen UI.&lt;/strong&gt; Ask for glare or defocus instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Seeds are how you get consistency&lt;/h2&gt;
&lt;p&gt;A seed makes a generation reproducible — same prompt plus same seed gives you the same clip. Two consequences worth knowing.&lt;/p&gt;
&lt;p&gt;First, running 3–5 seeds per hero shot is not waste; it is how you get a choice. The model's output varies enough between seeds that the third attempt is often materially better than the first, and you cannot tell which without generating them.&lt;/p&gt;
&lt;p&gt;Second, once you find a seed that works for a subject, reuse it across related shots. That is the closest thing to character consistency these models offer, and it costs nothing extra.&lt;/p&gt;
&lt;p&gt;Worth being deliberate about, though: seeds multiply spend. In a batch, video shots get multiple seeds and stills usually get one — a still that is nearly right can be fixed in an image editor, where a video clip cannot.&lt;/p&gt;
&lt;h2&gt;Putting it together&lt;/h2&gt;
&lt;p&gt;Apply the anatomy across a whole shot list and the output stops being a lucky dip. Every row carries the same structure, the same grade language and the same motion discipline, so the clips cut together as if they were shot by one person on one day — which is the actual goal.&lt;/p&gt;
&lt;p&gt;The next question is what all that generation costs, and it is not what the pricing page suggests.&lt;/p&gt;
&lt;h2&gt;Related posts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/automate-ai-video-creation-claude-higgsfield" rel="noopener noreferrer"&gt;How to Automate AI Video Creation with Claude &amp;amp; Higgsfield&lt;/a&gt; — the pipeline these prompts feed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/higgsfield-credits-vs-unlimited" rel="noopener noreferrer"&gt;Higgsfield Credits vs Unlimited: Real Per-Clip Cost Breakdown&lt;/a&gt; — what each of those seeds actually costs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/higgsfield-python-batch-queue-runner" rel="noopener noreferrer"&gt;Automate Higgsfield with a Python Batch Queue Runner&lt;/a&gt; — running the whole list hands-off&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://dineshstack.com/tag/ai-video-automation" rel="noopener noreferrer"&gt;AI Video Automation&lt;/a&gt; series&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Automate AI Video Creation with Claude &amp; Higgsfield</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Tue, 04 Aug 2026 20:33:51 +0000</pubDate>
      <link>https://dev.to/dineshstack/how-to-automate-ai-video-creation-with-claude-higgsfield-d2j</link>
      <guid>https://dev.to/dineshstack/how-to-automate-ai-video-creation-with-claude-higgsfield-d2j</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Claude writes and manages the prompts, the Higgsfield CLI generates the clips, and DaVinci Resolve's Python API assembles the edit. Everything is driven by one CSV shot list. The constraint that shapes the whole design: Higgsfield's "unlimited" applies to the web UI only — the CLI bills credits on every generation.&lt;/p&gt;
&lt;p&gt;This is the hub post for a series documenting a real AI-video pipeline: one I built to produce cinematic marketing clips at scale, then ran overnight against a metered API. Below is the whole architecture, the file format that holds it together, and the constraints that dictated most of the design decisions.&lt;/p&gt;
&lt;h2&gt;What the pipeline does&lt;/h2&gt;
&lt;p&gt;The goal is narrow: turn a list of scene descriptions into a folder of finished clips with as little clicking as possible. Three tools, three jobs.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Produces&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes and manages cinematic prompts, and the automation code around them&lt;/td&gt;
&lt;td&gt;A prompt library and the batch runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Higgsfield CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates clips via Seedance 2.0, Kling 3.0, Nano Banana Pro&lt;/td&gt;
&lt;td&gt;Raw video and image files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DaVinci Resolve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Assembles clips into a finished edit via its Python API&lt;/td&gt;
&lt;td&gt;The rendered video&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Claude does not generate video. It orchestrates: prompt authoring at volume, and the glue code that turns a list into a run. The rendering happens in Higgsfield's models.&lt;/p&gt;
&lt;h2&gt;How the pieces connect&lt;/h2&gt;
&lt;p&gt;Everything flows through one structured file — a CSV shot list. That single source of truth is what makes the pipeline reproducible rather than a sequence of remembered steps:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot list (CSV)  -&amp;gt;  Higgsfield CLI (generate)  -&amp;gt;  clips/ + manifest&lt;br&gt;
                                                        |&lt;br&gt;
                                                        v&lt;br&gt;
                                        DaVinci Resolve (Python) -&amp;gt; final.mp4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generator walks the list. The editor reads the manifest of finished clips and lays them onto a timeline. No stage depends on a human remembering what came before, which is the only way an overnight run is possible.&lt;/p&gt;
&lt;h3&gt;The shot list schema&lt;/h3&gt;
&lt;p&gt;This is the actual header the batch runner expects. It is worth getting right early, because every later stage reads from it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id,product,category,model,prompt,aspect,duration,mode,resolution,priority&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two columns do more work than they look like they do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;category&lt;/strong&gt;&lt;/code&gt; — &lt;code&gt;video&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt; or &lt;code&gt;audio&lt;/code&gt;. This is what lets the runner split the list across separate worker queues rather than processing everything in one line.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;priority&lt;/strong&gt;&lt;/code&gt; — because a run gets interrupted. Ordering by priority means the shots that matter are already done when it does.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;model&lt;/code&gt; being per-row is deliberate too. A hero shot and a background plate do not need the same model, and as the cost table below shows, pretending they do is expensive.&lt;/p&gt;
&lt;h2&gt;The constraint that shaped everything: credits vs unlimited&lt;/h2&gt;
&lt;p&gt;Higgsfield advertises unlimited generation. That is true — &lt;strong&gt;in the web UI&lt;/strong&gt;. The &lt;strong&gt;CLI and API always bill credits&lt;/strong&gt;. Their own pricing page states it plainly:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Unlimited models and Free Generations are accessible only via higgsfield.ai and are not accessible on MCP/CLI, Canvas or Supercomputer.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I confirmed it the expensive way: one Nano Banana Pro image through the CLI dropped my balance from 10 credits to 8.&lt;/p&gt;
&lt;p&gt;This matters more than it first appears. Automation means the CLI. So the moment you build a pipeline, you are on the metered path — and every design instinct you would carry over from an unlimited tier is wrong. You cannot over-generate and pick the best. You generate one strong take per shot and regenerate only failures.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 2.0, 1080p 5s&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 2.0, 720p 5s&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 pro, 5s video&lt;/td&gt;
&lt;td&gt;12.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 std, 5s video&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 std, 3s video&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nano Banana Pro image, 2K&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Note the top two rows: dropping Seedance from 1080p to 720p halves the cost. On a library of forty-odd shots that single decision is the difference between one plan tier and the next. Always price a shot before batching it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;higgsfield generate cost seedance_2_0 --duration 5 --resolution 1080p --prompt "test"

&lt;/code&gt;&lt;h1&gt;&lt;code&gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  45 credits — drop to 720p and it is 22.5&lt;/code&gt;&lt;/h1&gt;&lt;/pre&gt;
&lt;h2&gt;Two constraints that dictated the architecture&lt;/h2&gt;
&lt;p&gt;Beyond cost, two properties of the CLI shaped how the batch runner had to be written. Both are the kind of thing you only discover by running it.&lt;/p&gt;
&lt;h3&gt;1. The relaxed queue allows one video and one image at a time&lt;/h3&gt;
&lt;p&gt;On a trial, concurrency is not "how many workers can I spawn" — it is two slots, one per media type. That maps to exactly two worker threads, each draining its own queue sequentially. More workers do not go faster; they just queue behind each other. This is why the shot list has a &lt;code&gt;category&lt;/code&gt; column: it is the key the runner splits on.&lt;/p&gt;
&lt;h3&gt;2. The CLI does not reliably save to disk&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;higgsfield generate create ... --wait --json&lt;/code&gt; blocks until the generation finishes and returns JSON containing a result URL. The output is URL-based, so the pipeline downloads the file itself rather than trusting the CLI to write it.&lt;/p&gt;
&lt;p&gt;That one detail has consequences everywhere: the runner needs its own download step, its own retry logic around that download, and a disk-space guard — because an overnight run that fills the volume at 3am fails in a much worse way than one that stops cleanly.&lt;/p&gt;
&lt;h2&gt;Why these three tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; — prompt engineering at volume, and writing the glue. Forty cinematic prompts that share a house style is a language problem, not a video problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higgsfield&lt;/strong&gt; — a multi-model aggregator, so one CLI reaches Seedance 2.0 and Kling 3.0 for video, Nano Banana Pro for stills, and text-to-speech, each selected per row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DaVinci Resolve&lt;/strong&gt; — free, professional, and scriptable through a Python API, so assembly automates too instead of being a manual timeline drag at the end.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The aggregator point is the one worth dwelling on. Integrating three video models directly means three APIs, three auth schemes and three billing models. One CLI with a &lt;code&gt;model&lt;/code&gt; column in a CSV is a materially smaller system.&lt;/p&gt;
&lt;h2&gt;What a run actually looks like&lt;/h2&gt;
&lt;p&gt;Three phases, and the first two exist because the third costs money:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dry run.&lt;/strong&gt; Walk the shot list and print every command without generating anything. Catches malformed rows and bad model names for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure one.&lt;/strong&gt; Generate a single clip and check the credit delta against the estimate. This is how you find out that a row is 45 credits rather than 22.5 before multiplying it by forty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full run.&lt;/strong&gt; Both slots saturated, retries with backoff, resume from the manifest if it dies.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Resume-from-manifest is not optional at this scale. A run that cannot resume is a run you cannot afford to interrupt, and something always interrupts it.&lt;/p&gt;
&lt;h2&gt;Sizing the spend&lt;/h2&gt;
&lt;p&gt;A roughly 46-shot cinematic library, one take each — Kling pro for hero shots, Seedance 720p for backgrounds, cheap stills where a still will do — comes to about &lt;strong&gt;750–1,000 credits&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Which makes plan choice straightforward: a 1,200-credit tier covers one full library pass plus around 35 regenerations, which is the right shape for a single project. A 3,000-credit tier only makes sense if you genuinely want two or three complete takes of everything. And if budget is zero and time is not, do it by hand in the web UI on a trial — unlimited, but slow and entirely manual.&lt;/p&gt;
&lt;p&gt;The rule that falls out of all of this: &lt;strong&gt;explore free in the web UI, then pay for credits only once you automate.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;What the rest of the series covers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt; — the architecture, the shot list, and the credit model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cinematic prompt formula&lt;/strong&gt; — the prompt anatomy that produces usable Seedance and Kling output instead of dreamlike mush.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits vs unlimited&lt;/strong&gt; — the full cost table and when each path makes sense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python batch queue runner&lt;/strong&gt; — generating a whole library hands-off, with retries, resume and the two-slot worker model described above.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related posts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/cinematic-ai-video-prompts-formula" rel="noopener noreferrer"&gt;Cinematic AI Video Prompts: A Step-by-Step Formula&lt;/a&gt; — the prompt anatomy behind every row in the shot list&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/higgsfield-credits-vs-unlimited" rel="noopener noreferrer"&gt;Higgsfield Credits vs Unlimited: Real Per-Clip Cost Breakdown&lt;/a&gt; — the measured costs in full&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dineshstack.com/en/higgsfield-python-batch-queue-runner" rel="noopener noreferrer"&gt;Automate Higgsfield with a Python Batch Queue Runner&lt;/a&gt; — the runner that executes the shot list&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://dineshstack.com/tag/ai-video-automation" rel="noopener noreferrer"&gt;AI Video Automation&lt;/a&gt; series&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Debug GitHub Actions CI Failures Systematically</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Mon, 03 Aug 2026 05:00:08 +0000</pubDate>
      <link>https://dev.to/dineshstack/how-to-debug-github-actions-ci-failures-systematically-4lme</link>
      <guid>https://dev.to/dineshstack/how-to-debug-github-actions-ci-failures-systematically-4lme</guid>
      <description>&lt;p&gt;CI failures have a reputation for being cryptic. After years of staring at GitHub Actions logs, I've found that 90% of all failures fall into one of five categories. If you know the categories, you can diagnose any failure in under 10 minutes.&lt;/p&gt;
&lt;h2&gt;Step 1: Read the Full Log&lt;/h2&gt;
&lt;p&gt;GitHub Actions collapses log sections by default. Always expand the failing step and scroll to the &lt;strong&gt;first&lt;/strong&gt; error, not the last. PHP and Laravel produce cascading errors — one root cause produces 50 error lines. The one that matters is always at the top.&lt;/p&gt;
&lt;h2&gt;The Five Failure Classes&lt;/h2&gt;
&lt;h3&gt;Class 1: Missing File or Directory&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;No such file or directory&lt;/code&gt;, &lt;code&gt;Please provide a valid cache path&lt;/code&gt;, &lt;code&gt;Class not found&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; A file exists on your machine but was never committed to git. Common culprits: storage skeleton directories, empty test directories, generated config files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Run &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git ls-files --others --exclude-standard&lt;/code&gt; locally. Commit what's missing. For directories, add a &lt;code&gt;.gitignore&lt;/code&gt; placeholder so git tracks the folder.&lt;/p&gt;
&lt;h3&gt;Class 2: Wrong Connection String&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;SQLSTATE[HY000] [2002] Connection refused&lt;/code&gt;, &lt;code&gt;cURL error 7: Failed to connect&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; CI is trying to connect to a service (database, Redis, Reverb, Pusher, Mailpit) that isn't running in CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add the service to your workflow &lt;code&gt;services:&lt;/code&gt; block, OR set the connection to a CI-appropriate driver:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo "BROADCAST_CONNECTION=log" &amp;gt;&amp;gt; .env.testing
echo "QUEUE_CONNECTION=sync" &amp;gt;&amp;gt; .env.testing
echo "MAIL_MAILER=array" &amp;gt;&amp;gt; .env.testing
echo "SCOUT_DRIVER=null" &amp;gt;&amp;gt; .env.testing&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Class 3: Missing Environment Variable&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;Undefined array key&lt;/code&gt;, a 500 where a config value was expected, &lt;code&gt;Target class [X] does not exist&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Your &lt;code&gt;.env.example&lt;/code&gt; is missing a key that the application reads. Works locally because your real &lt;code&gt;.env&lt;/code&gt; has it; fails in CI because CI starts from &lt;code&gt;.env.example&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add the missing key to &lt;code&gt;.env.example&lt;/code&gt; with a safe default value.&lt;/p&gt;
&lt;h3&gt;Class 4: SQL Dialect Mismatch&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Tests pass locally (SQLite), fail in CI (MySQL). &lt;code&gt;FUNCTION X does not exist&lt;/code&gt;, &lt;code&gt;Ambiguous column 'status'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Raw SQL written against SQLite syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Replace with MySQL equivalents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Wrong (SQLite)
-&amp;gt;selectRaw("first_name || ' ' || last_name as full_name")
-&amp;gt;selectRaw("strftime('%Y-%m', created_at) as month")

// Correct (MySQL)
-&amp;gt;selectRaw("CONCAT(first_name, ' ', last_name) as full_name")
-&amp;gt;selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month")&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Class 5: Tooling Version Difference&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; A step that worked last week suddenly fails. Different error messages than your local run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; A dependency was updated — PHPStan 1.x → 2.x dropped config options; ESLint 9 removed legacy commands; PHP 8.4 changed behaviour of some functions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pin major versions in &lt;code&gt;composer.json&lt;/code&gt;/&lt;code&gt;package.json&lt;/code&gt; for tools that release breaking changes. Check the changelog when a step breaks with a "deprecated option" or "unknown argument" error.&lt;/p&gt;
&lt;h2&gt;Reproducing CI Locally&lt;/h2&gt;
&lt;p&gt;The fastest way to debug CI is to reproduce it locally with the same env vars:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a clean test database
mysql -uroot -p -e "DROP DATABASE IF EXISTS your_app_test; CREATE DATABASE your_app_test;"

# Export the same env vars CI uses
export DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306
export DB_DATABASE=your_app_test DB_USERNAME=root DB_PASSWORD=secret
export QUEUE_CONNECTION=sync BROADCAST_CONNECTION=log

# Run the exact commands CI runs
composer install --no-interaction --prefer-dist
php artisan migrate --env=testing --force
./vendor/bin/pest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it fails here, iterate fast. If it passes here but fails in CI, the difference is in one of the five classes above.&lt;/p&gt;
&lt;h2&gt;Reading Exit Codes&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tests ran, some failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configuration error — Pest: test directory not found; PHPStan: config error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;127&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command not found — binary not installed or not in PATH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;130&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process killed — usually OOM, increase &lt;code&gt;--memory-limit&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Exit code 2 from Pest means it never reached your tests. Check that every directory listed in &lt;code&gt;phpunit.xml&lt;/code&gt; exists in git and contains at least one test file.&lt;/p&gt;
&lt;h2&gt;The Diagnostic Checklist&lt;/h2&gt;
&lt;p&gt;When CI goes red:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Expand the failing step log and find the &lt;strong&gt;first&lt;/strong&gt; error&lt;/li&gt;
&lt;li&gt;Identify which of the five classes it belongs to&lt;/li&gt;
&lt;li&gt;Check if it's exit code 2 (config) vs 1 (test failure)&lt;/li&gt;
&lt;li&gt;Reproduce locally with the same env vars&lt;/li&gt;
&lt;li&gt;Fix, push, watch CI — don't guess and iterate blindly&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With this system you go from red CI to root cause in minutes, not hours. That's the whole job — build the pipeline once, trust it forever.&lt;/p&gt;
&lt;p&gt;This is the final post in the series. If you followed from Post 1 you now have: a GitHub Actions workflow running on every push, Pest tests against real MySQL, PHPStan and Pint quality gates, secure secrets management, zero-downtime VPS deployment, and the debugging playbook for when things go wrong. That's a production-grade CI/CD pipeline — the same foundation I put on every serious Laravel project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/debug-github-actions-ci-failures-laravel?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>debugging</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>Next.js Build Output Symbols Explained: , , ƒ and</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Sat, 01 Aug 2026 17:10:55 +0000</pubDate>
      <link>https://dev.to/dineshstack/nextjs-build-output-symbols-explained-f-and--6n9</link>
      <guid>https://dev.to/dineshstack/nextjs-build-output-symbols-explained-f-and--6n9</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Next.js 16 prints four symbols in its build output — &lt;code&gt;○&lt;/code&gt; Static, &lt;code&gt;●&lt;/code&gt; SSG, &lt;code&gt;ƒ&lt;/code&gt; Dynamic, and &lt;code&gt;◐&lt;/code&gt; Partial Prerender. If you are looking for &lt;code&gt;λ&lt;/code&gt;, it no longer exists; it was replaced by &lt;code&gt;ƒ&lt;/code&gt; in Next.js 14.1.&lt;/p&gt;
&lt;p&gt;You run &lt;code&gt;next build&lt;/code&gt;, and the route table comes back covered in symbols nobody explained:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route (app)                              Size     First Load JS
┌ ○ /                                    5.02 kB         112 kB
├ ○ /_not-found                            977 B         103 kB
├ ƒ /en/[slug]                           8.14 kB         128 kB
├ ● /blog                                3.71 kB         109 kB
└ ◐ /dashboard                           12.4 kB         141 kB

○  (Static)             prerendered as static content
●  (SSG)                prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)            server-rendered on demand
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The legend is printed underneath, but it only lists the symbols your build actually used — so if you have no partially prerendered routes, you never see &lt;code&gt;◐&lt;/code&gt; explained, and the first time one appears it looks like an error. Worse, most articles about this still document &lt;code&gt;λ&lt;/code&gt;, which current Next.js does not emit at all.&lt;/p&gt;
&lt;p&gt;Below is what each one means, and — more usefully — the exact decision tree Next.js walks to choose one, taken from the source in &lt;code&gt;next/dist/build/utils.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The four symbols&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;What Next.js says&lt;/th&gt;
&lt;th&gt;What it means for you&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;○&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;prerendered as static content&lt;/td&gt;
&lt;td&gt;Built once at build time. Served from disk or CDN. Fastest possible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;●&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSG&lt;/td&gt;
&lt;td&gt;prerendered as static HTML (uses &lt;code&gt;getStaticProps&lt;/code&gt; / &lt;code&gt;generateStaticParams&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Also built ahead of time, but from a data-fetching function you wrote.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ƒ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;td&gt;server-rendered on demand&lt;/td&gt;
&lt;td&gt;Runs on every request. Nothing is cached ahead of time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;◐&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial Prerender&lt;/td&gt;
&lt;td&gt;prerendered as static HTML with dynamic server-streamed content&lt;/td&gt;
&lt;td&gt;A static shell ships instantly, dynamic holes stream in after.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;Why ○ and ● are both "static"&lt;/h3&gt;
&lt;p&gt;This is the distinction that confuses people, because both are prerendered at build time and both serve instantly.&lt;/p&gt;
&lt;p&gt;The difference is where the content came from. &lt;code&gt;○&lt;/code&gt; is a route with no data-fetching function at all — an about page, a privacy policy, anything Next.js can render purely from your components. &lt;code&gt;●&lt;/code&gt; is a route that ran a data function at build time, so Next.js generated HTML from an external source: your CMS, your database, a filesystem read.&lt;/p&gt;
&lt;p&gt;Practically: if &lt;code&gt;●&lt;/code&gt; appears on a route, that route's content is frozen at build time, and changing the underlying data does nothing until you rebuild or revalidate.&lt;/p&gt;
&lt;h2&gt;The decision tree Next.js actually uses&lt;/h2&gt;
&lt;p&gt;This is the part no explainer covers. Symbol selection is not a lookup — it is a chain of conditions evaluated in a specific order, and the order is why routes sometimes get a symbol you did not expect. Reduced from the Next.js 16 source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (pageInfo?.runtime) {
    symbol = 'ƒ';                        // an explicit runtime always wins
} else if (pageInfo?.isRoutePPREnabled) {
    if (isDynamicAppRoute &amp;amp;&amp;amp; !pageInfo.hasPostponed) {
        symbol = 'ƒ';                    // PPR on, but nothing was deferred
    } else if (!pageInfo?.hasPostponed) {
        symbol = '○';                    // PPR on, fully static after all
    } else {
        symbol = '◐';                    // PPR on, and something was postponed
    }
} else if (pageInfo?.isStatic) {
    symbol = '○';
} else if (pageInfo?.isSSG) {
    symbol = '●';
} else {
    symbol = 'ƒ';                        // the fallback
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three things fall out of this that are worth knowing.&lt;/p&gt;
&lt;h3&gt;1. ƒ is the fallback, not a diagnosis&lt;/h3&gt;
&lt;p&gt;Look at the final &lt;code&gt;else&lt;/code&gt;. If Next.js cannot prove a route is static, it gets &lt;code&gt;ƒ&lt;/code&gt;. That means &lt;code&gt;ƒ&lt;/code&gt; does not tell you why a route is dynamic — only that nothing qualified it as static. A single &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or &lt;code&gt;searchParams&lt;/code&gt; access anywhere in the tree is enough, and the build output will not name the culprit.&lt;/p&gt;
&lt;h3&gt;2. Setting a runtime short-circuits everything&lt;/h3&gt;
&lt;p&gt;The very first condition is &lt;code&gt;pageInfo?.runtime&lt;/code&gt;. If you have declared a runtime on a route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const runtime = 'edge';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…it gets &lt;code&gt;ƒ&lt;/code&gt; immediately, before any static analysis runs. A route that would otherwise have been fully static becomes dynamic purely because you named a runtime. If you see an unexpected &lt;code&gt;ƒ&lt;/code&gt; on a page with no dynamic APIs, check for a stray &lt;code&gt;runtime&lt;/code&gt; export first.&lt;/p&gt;
&lt;h3&gt;3. PPR routes can show any of three symbols&lt;/h3&gt;
&lt;p&gt;With Partial Prerendering enabled, a route only earns &lt;code&gt;◐&lt;/code&gt; if something was actually postponed — that is, if Next.js found dynamic content to defer. Enable PPR on a route with nothing dynamic in it and you get plain &lt;code&gt;○&lt;/code&gt;, because there was no hole to stream. Enable it on a fully dynamic route and you get &lt;code&gt;ƒ&lt;/code&gt;. Seeing &lt;code&gt;○&lt;/code&gt; on a route you configured for PPR is not a misconfiguration; it means PPR had nothing to do.&lt;/p&gt;
&lt;h2&gt;What happened to λ?&lt;/h2&gt;
&lt;p&gt;Older Next.js used &lt;code&gt;λ&lt;/code&gt; (Server) for server-rendered routes. It was replaced by &lt;code&gt;ƒ&lt;/code&gt; in 14.1 — the lambda reference made less sense once routes ran in more places than serverless functions.&lt;/p&gt;
&lt;p&gt;I checked the installed Next.js 16.2.4 package directly: &lt;code&gt;λ&lt;/code&gt; appears zero times in the build output code. If a tutorial shows it, that tutorial predates 14.1. This matters when debugging, because searching for "next.js lambda symbol build" surfaces guidance for a symbol your build can no longer produce.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Server-rendered symbol&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≤ 14.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;λ&lt;/code&gt; (Server)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.1+&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ƒ&lt;/code&gt; (Dynamic)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;The Revalidate and Expire columns that come and go&lt;/h2&gt;
&lt;p&gt;You may have noticed the build table sometimes has extra columns and sometimes does not. That is deliberate, not a rendering glitch. Next.js builds the header like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    listType === 'app' ? 'Route (app)' : 'Route (pages)',
    showRevalidate ? 'Revalidate' : '',
    showExpire    ? 'Expire'     : '',
].filter(Boolean)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;showRevalidate&lt;/code&gt; and &lt;code&gt;showExpire&lt;/code&gt; are set by scanning every page for a cache-control value. If not one route in your build sets a revalidation window, the column is dropped entirely. So adding revalidation to a single route makes a new column appear across the whole table — nothing about your other routes changed.&lt;/p&gt;
&lt;p&gt;Also worth knowing if you are following an older tutorial: Next.js used to print an &lt;code&gt;ISR&lt;/code&gt; legend entry. In 16 that string does not exist in the build output at all — incremental regeneration is now communicated through these two columns instead of a symbol.&lt;/p&gt;
&lt;h3&gt;The revalidate: 0 trap&lt;/h3&gt;
&lt;p&gt;This one is genuinely easy to trip over. If a page uses &lt;code&gt;getStaticProps&lt;/code&gt; with &lt;code&gt;revalidate: 0&lt;/code&gt;, Next.js does not treat it as static-with-instant-revalidation. It reclassifies it as dynamic outright:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (hasGSPAndRevalidateZero.has(item)) {
    usedSymbols.add('ƒ');
    // ...route is rendered with ƒ in the table
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a page you wrote as SSG, which you would expect to show &lt;code&gt;●&lt;/code&gt;, shows &lt;code&gt;ƒ&lt;/code&gt; instead — and every request pays for a server render. If you wanted "always fresh but still cached briefly", use &lt;code&gt;revalidate: 1&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;. Zero means "never cache", and Next.js is telling you so through the symbol.&lt;/p&gt;
&lt;h2&gt;Reading your own build output&lt;/h2&gt;
&lt;p&gt;A healthy content site is mostly &lt;code&gt;○&lt;/code&gt; and &lt;code&gt;●&lt;/code&gt;, with &lt;code&gt;ƒ&lt;/code&gt; reserved for routes that genuinely need per-request data — dashboards, anything behind auth, anything reading cookies.&lt;/p&gt;
&lt;p&gt;If a route you expected to be static shows &lt;code&gt;ƒ&lt;/code&gt;, work through it in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check for a &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;runtime&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; export.&lt;/strong&gt; It short-circuits the whole chain, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for dynamic APIs&lt;/strong&gt; — &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, &lt;code&gt;connection()&lt;/code&gt;. One call anywhere in the rendered tree is enough, including inside a component three levels down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your fetch calls.&lt;/strong&gt; A fetch with &lt;code&gt;cache: 'no-store'&lt;/code&gt; opts the route out of static rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for a parent layout doing any of the above.&lt;/strong&gt; This is the one that catches people — a layout reading cookies makes every route beneath it dynamic, and the build output blames the child.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To see exactly which API forced it, build with more detail:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;next build --debug&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick reference&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Read it as&lt;/th&gt;
&lt;th&gt;Act if…&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;○&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Free. Served from CDN.&lt;/td&gt;
&lt;td&gt;Never — this is the goal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;●&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Free, but frozen at build time.&lt;/td&gt;
&lt;td&gt;Content looks stale — add revalidation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ƒ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Costs a server render per request.&lt;/td&gt;
&lt;td&gt;You expected static. Work the list above.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;◐&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Static shell + streamed dynamic holes.&lt;/td&gt;
&lt;td&gt;Never — this is usually what you want on a mixed page.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The symbols are not decoration. On a content site, the difference between &lt;code&gt;○&lt;/code&gt; and &lt;code&gt;ƒ&lt;/code&gt; across a few hundred routes is the difference between a CDN bill and a server bill.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/nextjs-build-output-symbols-explained?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Zero-Downtime Deploy to a VPS with GitHub Actions</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Fri, 31 Jul 2026 20:18:06 +0000</pubDate>
      <link>https://dev.to/dineshstack/zero-downtime-deploy-to-a-vps-with-github-actions-15a9</link>
      <guid>https://dev.to/dineshstack/zero-downtime-deploy-to-a-vps-with-github-actions-15a9</guid>
      <description>&lt;p&gt;Managed deployment services like Laravel Forge and Envoyer are excellent and I use them on larger client projects. But you don't need them. Everything they do is SSH commands and symlinks, and you can do those yourself for free.&lt;/p&gt;
&lt;p&gt;This is the deployment pattern I use on side projects and small client sites. It costs nothing beyond the VPS ($6/month on Hetzner or DigitalOcean).&lt;/p&gt;
&lt;h2&gt;The Releases Pattern&lt;/h2&gt;
&lt;p&gt;Instead of deploying directly to &lt;code&gt;/var/www/app&lt;/code&gt;, you deploy to a timestamped folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/www/app/
├── releases/
│   ├── 20260101120000/    ← previous deployment
│   └── 20260708093045/    ← current deployment
├── shared/
│   ├── .env               ← shared across all releases
│   └── storage/           ← shared uploads and logs
└── current -&amp;gt; releases/20260708093045/   ← symlink&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nginx points at &lt;code&gt;/var/www/app/current&lt;/code&gt;. When you deploy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone the new release into &lt;code&gt;releases/$(date +%Y%m%d%H%M%S)/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Link &lt;code&gt;shared/.env&lt;/code&gt; and &lt;code&gt;shared/storage/&lt;/code&gt; into the new release&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;composer install&lt;/code&gt;, &lt;code&gt;php artisan migrate --force&lt;/code&gt;, cache config/routes&lt;/li&gt;
&lt;li&gt;Flip the &lt;code&gt;current&lt;/code&gt; symlink to the new release (atomic — zero downtime)&lt;/li&gt;
&lt;li&gt;Reload PHP-FPM&lt;/li&gt;
&lt;li&gt;Delete releases older than the last 5&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If anything fails in steps 1–3, the symlink was never flipped. Users see nothing. Rollback is one command: &lt;code&gt;ln -sfn releases/previous current &amp;amp;&amp;amp; sudo systemctl reload php8.4-fpm&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Server Setup (One-Time)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Install PHP 8.4 and extensions
sudo apt-get install -y php8.4-fpm php8.4-cli php8.4-mbstring php8.4-pdo \
  php8.4-mysql php8.4-bcmath php8.4-gd php8.4-zip php8.4-intl

# Create directory structure
sudo mkdir -p /var/www/app/{releases,shared}
sudo chown -R www-data:www-data /var/www/app

# Create the deploy user and add SSH key
sudo adduser deploy --disabled-password
sudo mkdir -p /home/deploy/.ssh
# paste your CI public key into authorized_keys
sudo nano /home/deploy/.ssh/authorized_keys
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Deploy Script&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;scripts/deploy.sh&lt;/code&gt; in your repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

DEPLOY_PATH="/var/www/app"
RELEASE="$DEPLOY_PATH/releases/$(date +%Y%m%d%H%M%S)"
CURRENT="$DEPLOY_PATH/current"
SHARED="$DEPLOY_PATH/shared"
KEEP_RELEASES=5

echo "→ Creating release: $RELEASE"
mkdir -p "$RELEASE"
cp -r /tmp/app-release/. "$RELEASE/"

echo "→ Linking shared resources"
rm -rf "$RELEASE/storage"
ln -sfn "$SHARED/storage" "$RELEASE/storage"
ln -sfn "$SHARED/.env" "$RELEASE/.env"

echo "→ Installing dependencies"
cd "$RELEASE"
composer install --no-dev --optimize-autoloader --no-interaction --quiet

echo "→ Running migrations"
php artisan migrate --force

echo "→ Caching"
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

echo "→ Flipping symlink"
ln -sfn "$RELEASE" "$CURRENT"

echo "→ Reloading PHP-FPM"
sudo systemctl reload php8.4-fpm

echo "→ Cleaning old releases (keeping $KEEP_RELEASES)"
ls -1dt "$DEPLOY_PATH/releases/"* | tail -n +$((KEEP_RELEASES + 1)) | xargs rm -rf

echo "✓ Deployed: $RELEASE"&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The GitHub Actions Deploy Job&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: tests
    if: github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Upload release to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          source: "api/."
          target: "/tmp/app-release"

      - name: Run deploy script
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          script_stop: true
          script: bash /tmp/app-release/scripts/deploy.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Branch Protection: Locking the Gate&lt;/h2&gt;
&lt;p&gt;Once this is working, go to &lt;strong&gt;GitHub → Settings → Branches → Add rule for &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;main&lt;/strong&gt;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Require status checks to pass before merging&lt;/li&gt;
&lt;li&gt;Select: &lt;strong&gt;Code Quality&lt;/strong&gt; and &lt;strong&gt;Tests (PHP 8.4, MySQL 8.4)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Require branches to be up to date before merging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now nothing can reach production without passing your full CI pipeline. This is the complete loop: write code → push → CI validates → deploy. The last post covers what to do when that loop breaks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/zero-downtime-deploy-vps-github-actions-laravel?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>deployment</category>
      <category>devops</category>
    </item>
    <item>
      <title>Managing Secrets and Env Vars in GitHub Actions</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Tue, 28 Jul 2026 02:00:06 +0000</pubDate>
      <link>https://dev.to/dineshstack/managing-secrets-and-env-vars-in-github-actions-4e6l</link>
      <guid>https://dev.to/dineshstack/managing-secrets-and-env-vars-in-github-actions-4e6l</guid>
      <description>&lt;p&gt;The most common beginner mistake with GitHub Actions secrets isn't accidentally committing them — it's putting them in the wrong place and then wondering why the deployment job can't read them.&lt;/p&gt;
&lt;h2&gt;Three Layers of Secrets in GitHub Actions&lt;/h2&gt;
&lt;h3&gt;1. Repository Secrets&lt;/h3&gt;
&lt;p&gt;Available to all workflows in the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub repo → Settings → Secrets and variables → Actions → New repository secret&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use for: &lt;code&gt;CODECOV_TOKEN&lt;/code&gt;, API keys used in tests, non-environment-specific values.&lt;/p&gt;
&lt;h3&gt;2. Environment Secrets&lt;/h3&gt;
&lt;p&gt;Only available when a job targets a specific environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub repo → Settings → Environments → New environment → Add secret&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use for: &lt;code&gt;DB_PASSWORD&lt;/code&gt; (different for staging vs production), &lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt; (different server per environment), &lt;code&gt;APP_KEY&lt;/code&gt; (must be different per environment).&lt;/p&gt;
&lt;h3&gt;3. Variables (Not Secrets)&lt;/h3&gt;
&lt;p&gt;Plaintext, visible in logs. Use for: &lt;code&gt;APP_URL&lt;/code&gt;, &lt;code&gt;APP_ENV&lt;/code&gt;, non-sensitive configuration.&lt;/p&gt;
&lt;h2&gt;The .env.example Contract&lt;/h2&gt;
&lt;p&gt;Your &lt;code&gt;.env.example&lt;/code&gt; is a contract. Every key your application reads must appear there. Every CI workflow copies this file and overrides specific values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rules for &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;.env.example&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All sensitive values: &lt;code&gt;APP_KEY=&lt;/code&gt;, &lt;code&gt;DB_PASSWORD=&lt;/code&gt;, &lt;code&gt;STRIPE_KEY=&lt;/code&gt; — left empty, filled by CI or developer&lt;/li&gt;
&lt;li&gt;All non-sensitive defaults filled in: &lt;code&gt;APP_NAME=MyApp&lt;/code&gt;, &lt;code&gt;DB_CONNECTION=mysql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every key that exists in production must exist here&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Wrong pattern — hardcoding secrets in YAML:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: Run tests
        run: ./vendor/bin/pest
        env:
          DB_PASSWORD: supersecretpassword123   # visible in git history forever&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Right pattern — using secrets:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: Run tests
        run: ./vendor/bin/pest
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Passport / OAuth Keys&lt;/h2&gt;
&lt;p&gt;Laravel Passport generates RSA key pairs (&lt;code&gt;oauth-private.key&lt;/code&gt;, &lt;code&gt;oauth-public.key&lt;/code&gt;). These are gitignored. In CI, generate them fresh each run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: Generate Passport keys
        run: php artisan passport:keys --force&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this step after &lt;code&gt;composer install&lt;/code&gt;, before migrations or tests. The &lt;code&gt;--force&lt;/code&gt; flag overwrites if they already exist — safe in CI since the runner starts fresh every time.&lt;/p&gt;
&lt;p&gt;For production, generate once locally and store the values as base64-encoded environment secrets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run locally, then store the output as GitHub environment secrets
php artisan passport:keys
cat storage/oauth-private.key | base64
cat storage/oauth-public.key | base64&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your deploy script, decode them back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo "$PASSPORT_PRIVATE_KEY" | base64 -d &amp;gt; storage/oauth-private.key
echo "$PASSPORT_PUBLIC_KEY" | base64 -d &amp;gt; storage/oauth-public.key&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using Environment Secrets in Deploy Jobs&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: tests
    if: github.ref == 'refs/heads/main'
    environment: production   # unlocks the 'production' environment secrets

    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;environment: production&lt;/code&gt; line is what unlocks the secrets you added under the production environment in GitHub settings. If you omit it, the job can only access repository-level secrets.&lt;/p&gt;
&lt;h2&gt;What to Never Put in YAML&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Real passwords — use &lt;code&gt;${{ secrets.X }}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Production API keys — use &lt;code&gt;${{ secrets.X }}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Private keys — base64-encode, store as secret&lt;/li&gt;
&lt;li&gt;Production &lt;code&gt;APP_KEY&lt;/code&gt; — environment secret per environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The test DB password (&lt;code&gt;secret&lt;/code&gt;) is fine in plain YAML because the MySQL container only lives for the duration of the CI job and is not accessible from outside the runner network.&lt;/p&gt;
&lt;p&gt;In the next post we wire up the final piece: a deploy job that SSHs into your VPS and ships the new release with zero downtime.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/github-actions-secrets-env-vars-laravel?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>Code Quality Gates: Laravel Pint and PHPStan in CI</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Sat, 25 Jul 2026 00:00:11 +0000</pubDate>
      <link>https://dev.to/dineshstack/code-quality-gates-laravel-pint-and-phpstan-in-ci-5fpg</link>
      <guid>https://dev.to/dineshstack/code-quality-gates-laravel-pint-and-phpstan-in-ci-5fpg</guid>
      <description>&lt;p&gt;The moment you add a code quality gate to CI, something quietly changes on your team. Developers stop arguing about code style in reviews because the robot handles it. Static-analysis errors stop reaching production because they can't pass the gate. You stop inheriting other people's technical debt because new errors are blocked immediately, even if old ones exist.&lt;/p&gt;
&lt;p&gt;That last point is the PHPStan baseline trick. We'll get to it.&lt;/p&gt;
&lt;h2&gt;The Code Quality Job&lt;/h2&gt;
&lt;p&gt;This job runs on every push and is intentionally fast — no database, no migrations, just PHP and a vendor folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  quality:
    name: Code Quality
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP 8.4
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, pdo, bcmath, zip, intl
          coverage: none
          tools: composer:v2

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: api/vendor
          key: php-8.4-composer-${{ hashFiles('api/composer.lock') }}
          restore-keys: php-8.4-composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --no-progress

      - name: Check formatting (Laravel Pint)
        run: ./vendor/bin/pint --test

      - name: Static analysis (PHPStan)
        run: ./vendor/bin/phpstan analyse --memory-limit=2G --no-progress&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pint --test&lt;/code&gt; runs in read-only mode — it checks without modifying files. Exit code 1 if any file has style drift. Run &lt;code&gt;./vendor/bin/pint&lt;/code&gt; locally (without &lt;code&gt;--test&lt;/code&gt;) to auto-fix before pushing.&lt;/p&gt;
&lt;h2&gt;Configuring PHPStan&lt;/h2&gt;
&lt;p&gt;Add a &lt;code&gt;phpstan.neon&lt;/code&gt; to your API root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parameters:
    level: 8
    paths:
        - app
        - Modules
    excludePaths:
        - vendor
    ignoreErrors:
        - identifier: missingType.iterableValue
        - identifier: missingType.generics&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Level 8 is strict. On a fresh project, aim for it from day one. On an existing codebase, start at level 4 and raise it gradually.&lt;/p&gt;
&lt;h3&gt;The Memory Problem&lt;/h3&gt;
&lt;p&gt;PHPStan at level 8 on a large codebase needs more than 512MB of RAM. Use &lt;code&gt;--memory-limit=2G&lt;/code&gt; in CI. Locally, add a shortcut to &lt;code&gt;composer.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"scripts": {
    "analyse": "php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress"
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Baseline Trick for Legacy Codebases&lt;/h2&gt;
&lt;p&gt;Here's the situation I encounter on almost every inherited project: PHPStan level 8 reports 2,600 existing errors. You can't fix them all in one PR. But you also can't ignore level 8 — you need it to catch new errors.&lt;/p&gt;
&lt;p&gt;The solution is a baseline file. You generate it once. It records all current errors as "accepted." From that point, any new error fails CI, but existing ones are silenced:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run once locally, commit the result
php -d memory_limit=2G vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then include it in &lt;code&gt;phpstan.neon&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;includes:
    - phpstan-baseline.neon

parameters:
    level: 8
    paths:
        - app
        - Modules&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit &lt;code&gt;phpstan-baseline.neon&lt;/code&gt;. From now on: CI passes on existing debt, fails on new debt. As you fix old errors, re-generate the baseline with fewer entries. Eventually you won't need it.&lt;/p&gt;
&lt;h2&gt;Ordering Jobs: Quality Before Tests&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;needs:&lt;/code&gt; to enforce ordering:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  tests:
    name: Tests (PHP 8.4, MySQL 8.4)
    needs: quality   # this line
    runs-on: ubuntu-latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the test job only starts if quality passed. This saves CI minutes — there's no point running a 3-minute test suite against code that won't pass style checks.&lt;/p&gt;
&lt;h2&gt;Developer Workflow With These Gates&lt;/h2&gt;
&lt;p&gt;The new routine before any push:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Fix style issues automatically
./vendor/bin/pint

# Check for type errors
php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress

# Run tests locally
php artisan test

# Now push — CI will agree
git push origin feature/my-feature&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a few weeks this becomes muscle memory. The quality gate stops being something that catches you and starts being something you pre-empt.&lt;/p&gt;
&lt;p&gt;In the next post we cover secrets and environment variables — the right way to handle API keys, database passwords, and OAuth keys in CI without ever putting sensitive values in your YAML files.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/laravel-pint-phpstan-github-actions-code-quality?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ci</category>
      <category>devops</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>How to Contribute to an Open-Source AI Trading Bot</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Fri, 24 Jul 2026 23:30:04 +0000</pubDate>
      <link>https://dev.to/dineshstack/how-to-contribute-to-an-open-source-ai-trading-bot-5a0d</link>
      <guid>https://dev.to/dineshstack/how-to-contribute-to-an-open-source-ai-trading-bot-5a0d</guid>
      <description>&lt;h1&gt;How to Contribute to an Open-Source AI Trading Bot&lt;/h1&gt;
&lt;p&gt;If you want to &lt;strong&gt;contribute to an open-source AI trading bot&lt;/strong&gt; — and build on a real, running system instead of a toy — this guide shows you exactly how. The Claude-powered crypto bot from &lt;a href="https://dineshstack.com/en/ai-crypto-trading-bot-claude" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; is MIT-licensed on GitHub, and the most interesting problems in it are wide open. You don’t need to be a quant or an ML expert; there’s meaningful work here for developers, traders, writers, and testers alike.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;👋 &lt;strong&gt;New to open source?&lt;/strong&gt; That’s fine — this is a friendly, low-pressure project. A thoughtful question or a docs fix is a real contribution.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Why Contribute to This Project?&lt;/h2&gt;
&lt;p&gt;Most “AI trading bot” repos are either abandoned demos or paywalled black boxes. This one is different: it’s a complete, documented, honestly-evaluated system where the central question — does it actually have a tradeable edge? — is genuinely unsolved. Contributing here means working on real LLM-agent orchestration, machine-learning signal modelling, exchange execution, and a production dashboard, with a maintainer who’ll actually review your PR. It’s a great portfolio piece and a great way to learn.&lt;/p&gt;
&lt;h2&gt;Where Help Is Most Needed&lt;/h2&gt;
&lt;p&gt;Pick whatever matches your skills:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Example contributions&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🧠 Strategy &amp;amp; research&lt;/td&gt;
&lt;td&gt;New signals, better entry/exit logic, ideas to capture trend (the current strategy is defensive and lags in bull markets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📈 ML modelling&lt;/td&gt;
&lt;td&gt;Feature engineering, calibration, honest walk-forward evaluation, reducing overfitting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛡️ Risk &amp;amp; execution&lt;/td&gt;
&lt;td&gt;Smarter sizing, OCO/bracket orders, slippage modelling, live-trading safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💻 Dashboard (Next.js)&lt;/td&gt;
&lt;td&gt;New visualizations, UX, mobile polish, accessibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔧 DevOps&lt;/td&gt;
&lt;td&gt;A one-command &lt;code&gt;docker compose&lt;/code&gt; setup — the single highest-impact task right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📖 Docs &amp;amp; testing&lt;/td&gt;
&lt;td&gt;Setup guides, tutorials, backtest rigor, unit tests, translations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Setting Up the Project Locally&lt;/h2&gt;
&lt;p&gt;You don’t need a VPS to contribute — run it locally on testnet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone &lt;a href="https://github.com/dineshstack/crypto_bot.git" rel="noopener noreferrer"&gt;https://github.com/dineshstack/crypto_bot.git&lt;/a&gt;&lt;br&gt;
cd crypto_bot&lt;br&gt;
python3 -m venv venv &amp;amp;&amp;amp; source venv/bin/activate&lt;br&gt;
pip install -r requirements.txt&lt;br&gt;
cp .env.example .env     # add your own keys; keep TESTNET=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the dashboard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd dashboard&lt;br&gt;
npm install&lt;br&gt;
cp .env.local.example .env.local&lt;br&gt;
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to Submit Your First Pull Request&lt;/h2&gt;
&lt;p&gt;The flow is standard GitHub — small, focused changes are the easiest to merge:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b feature/your-improvement

&lt;h1&gt;
  
  
  make your change
&lt;/h1&gt;

&lt;p&gt;python3 -m py_compile changed_file.py     # sanity-check Python&lt;br&gt;
git commit -m "Clear description of what changed and why"&lt;br&gt;
git push origin feature/your-improvement&lt;/p&gt;

&lt;/code&gt;&lt;h1&gt;&lt;code&gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  then open a Pull Request against main&lt;/code&gt;&lt;/h1&gt;&lt;/pre&gt;
&lt;p&gt;In your PR, describe &lt;strong&gt;what&lt;/strong&gt; you changed, &lt;strong&gt;why&lt;/strong&gt;, and &lt;strong&gt;how you tested it&lt;/strong&gt;. Anything touching order execution, sizing, or the circuit breakers gets extra review — describe your testing in detail, and never weaken a safety check without explaining why. The full checklist is in the repo’s &lt;a href="https://github.com/dineshstack/crypto_bot/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Not a Coder? You Can Still Help&lt;/h2&gt;
&lt;p&gt;Some of the most valuable contributions aren’t code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🐛 &lt;strong&gt;Report bugs&lt;/strong&gt; or unexpected behaviour with clear steps to reproduce.&lt;/li&gt;
&lt;li&gt;💡 &lt;strong&gt;Suggest strategy ideas&lt;/strong&gt; or share how it behaved in a market regime you tested.&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Improve the docs&lt;/strong&gt; — even a typo fix or a clearer sentence helps the next person.&lt;/li&gt;
&lt;li&gt;⭐ &lt;strong&gt;Star and share the repo&lt;/strong&gt; — reach is what brings in more contributors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Look for issues labelled &lt;code&gt;good first issue&lt;/code&gt; to get started.&lt;/p&gt;
&lt;h2&gt;Let’s Build Something Honest Together&lt;/h2&gt;
&lt;p&gt;This project is deliberately transparent about what it can and can’t do, which makes it a rare thing in the “AI trading” space: a place to genuinely learn and experiment without hype. If that appeals to you, jump in.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🚀 &lt;strong&gt;Ready to contribute?&lt;/strong&gt;&lt;br&gt;⭐ &lt;a href="https://github.com/dineshstack/crypto_bot" rel="noopener noreferrer"&gt;&lt;strong&gt;Star &amp;amp; fork the repo&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;💬 &lt;a href="https://github.com/dineshstack/crypto_bot/issues" rel="noopener noreferrer"&gt;&lt;strong&gt;Open an issue&lt;/strong&gt;&lt;/a&gt; describing what you’d like to work on&lt;br&gt;☕ Not contributing code but want to support the work? &lt;a href="https://ko-fi.com/dineshstack" rel="noopener noreferrer"&gt;&lt;strong&gt;Buy me a coffee on Ko-fi&lt;/strong&gt;&lt;/a&gt; — it keeps the demo and API running.&lt;/p&gt;&lt;/blockquote&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fik.imagekit.io%2Fu7tvtcis2%2Fscheduler%2Fgallery%2FOpen-source_AI_trading_bot_GitHub_repository_ready_for_contributors_1784922610_Xi2rT3O0L.png" width="800" height="710"&gt;&lt;img src="/images/ai-crypto-trading-bot-github-contribute.png" alt="Open-source AI trading bot GitHub repository ready for contributors"&gt;The repo is open, MIT-licensed, and waiting for your first pull request.
&lt;h3&gt;Related posts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dineshstack.com/en/ai-crypto-trading-bot-claude" rel="noopener noreferrer"&gt;Part 1 — How I Built an AI Crypto Trading Bot with Claude AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dineshstack.com/en/deploy-ai-crypto-trading-bot-vps" rel="noopener noreferrer"&gt;Part 2 — Deploy an AI Crypto Trading Bot to Your VPS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Open Source, AI Trading Bot, Contributing, GitHub, Developer Community&lt;/p&gt;
&lt;p&gt;Disclaimer: For educational and research purposes only. Not financial advice. Cryptocurrency trading carries substantial risk of loss.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>crypto</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Deploy an AI Crypto Trading Bot on Your VPS</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:45:04 +0000</pubDate>
      <link>https://dev.to/dineshstack/how-to-deploy-an-ai-crypto-trading-bot-on-your-vps-59a4</link>
      <guid>https://dev.to/dineshstack/how-to-deploy-an-ai-crypto-trading-bot-on-your-vps-59a4</guid>
      <description>&lt;h1&gt;How to Deploy an AI Crypto Trading Bot on Your VPS&lt;/h1&gt;
&lt;p&gt;In this tutorial you’ll &lt;strong&gt;deploy an AI crypto trading bot&lt;/strong&gt; to your own VPS — the open-source Claude-powered bot from &lt;a href="https://dineshstack.com/en/ai-crypto-trading-bot-claude" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; — and have it analysing BTC and ETH on Binance testnet, messaging you on Telegram, in about 20–30 minutes. I’ll give you the exact steps, a complete list of the API keys you need, an honest running-cost breakdown, and every gotcha I hit so you don’t lose an evening to them.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Start on testnet.&lt;/strong&gt; Everything below uses Binance testnet (fake money). Never point live keys at a bot you haven’t watched run for weeks. This is educational content, not financial advice.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before you deploy the AI crypto trading bot, get these ready:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A VPS&lt;/strong&gt; running Ubuntu 22.04 or 24.04. To run the bot, 2 vCPU / 4&amp;nbsp;GB RAM is plenty. To train the ML model, more RAM helps — or train it on your laptop and copy the model file over (more below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.12&lt;/strong&gt; and &lt;strong&gt;MySQL 8&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;li&gt;Accounts and API keys (the next section is a full table).&lt;/li&gt;
&lt;li&gt;Basic comfort with SSH and the command line.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The API keys you’ll need&lt;/h3&gt;
&lt;p&gt;This trips people up, so here’s everything in one place:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Where to get it&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API key&lt;/td&gt;
&lt;td&gt;console.anthropic.com&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Enable billing&lt;/strong&gt; — the free tier won’t sustain continuous calls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binance API key + secret&lt;/td&gt;
&lt;td&gt;Binance testnet: testnet.binance.vision&lt;/td&gt;
&lt;td&gt;For live later: spot-only, &lt;strong&gt;no withdrawal&lt;/strong&gt;, IP-restricted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram bot token&lt;/td&gt;
&lt;td&gt;&lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Create a bot, copy the token.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram chat ID&lt;/td&gt;
&lt;td&gt;@userinfobot&lt;/td&gt;
&lt;td&gt;Your personal chat ID — the bot only talks to you.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoinGecko API key&lt;/td&gt;
&lt;td&gt;coingecko.com/api&lt;/td&gt;
&lt;td&gt;Free tier is fine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Step 1: Set Up the Server&lt;/h2&gt;
&lt;p&gt;SSH into your VPS and install the essentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install -y python3.12 python3.12-venv python3-pip mysql-server libomp-dev

&lt;h1&gt;
  
  
  Create the database
&lt;/h1&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;sudo mysql -e "CREATE DATABASE crypto_bot;"&lt;br&gt;&lt;br&gt;
sudo mysql -e "CREATE USER 'crypto_bot'@'localhost' IDENTIFIED BY 'your-strong-password';"&lt;br&gt;&lt;br&gt;
sudo mysql -e "GRANT ALL ON crypto_bot.* TO 'crypto_bot'@'localhost';"&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Clone and Install the Bot&lt;/h2&gt;
&lt;p&gt;Clone the repo and run the install script, which sets up the Python virtual environment, installs dependencies, and registers a systemd service that auto-restarts on crash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone &lt;a href="https://github.com/dineshstack/crypto_bot.git" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://github.com/dineshstack/crypto_bot.git" rel="noopener noreferrer"&gt;https://github.com/dineshstack/crypto_bot.git&lt;/a&gt;&lt;br&gt;&lt;br&gt;
cd crypto_bot&lt;br&gt;&lt;br&gt;
bash deploy/install.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Configure Your API Keys&lt;/h2&gt;
&lt;p&gt;Copy the example environment file and fill in the keys from the table above. &lt;strong&gt;Never commit this file&lt;/strong&gt; — it’s git-ignored for a reason.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp .env.example .env&lt;br&gt;&lt;br&gt;
nano .env&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ANTHROPIC_API_KEY=sk-ant-...&lt;br&gt;&lt;br&gt;
BINANCE_API_KEY=your_testnet_key&lt;br&gt;&lt;br&gt;
BINANCE_SECRET=your_testnet_secret&lt;br&gt;&lt;br&gt;
TELEGRAM_BOT_TOKEN=your_bot_token&lt;br&gt;&lt;br&gt;
TELEGRAM_CHAT_ID=your_chat_id&lt;br&gt;&lt;br&gt;
MYSQL_PASSWORD=your-strong-password&lt;br&gt;&lt;br&gt;
TESTNET=true          # keep this true&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 4: Train the ML Model&lt;/h2&gt;
&lt;p&gt;The bot ships without a trained model — you train it once on historical data. This is the only CPU/RAM-heavy step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source venv/bin/activate&lt;br&gt;&lt;br&gt;
python3 -c "import ml_signal, market_data as md; ml_signal.train_model(md.get_exchange())"&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Low-RAM VPS?&lt;/strong&gt; Run this same command on your laptop, then copy the generated &lt;code&gt;ml_models/&lt;/code&gt; folder to the server with &lt;code&gt;scp&lt;/code&gt;. The bot’s runtime is light; only training is hungry.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Step 5: Start the Bot and Connect Telegram&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start crypto-bot&lt;br&gt;&lt;br&gt;
sudo systemctl enable crypto-bot   # auto-start on reboot&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now open Telegram, find your bot, and send &lt;code&gt;/start&lt;/code&gt;. Within a few minutes you’ll get your first analysis message. Useful commands: &lt;code&gt;/status&lt;/code&gt;, &lt;code&gt;/analyze&lt;/code&gt;, &lt;code&gt;/performance&lt;/code&gt;, &lt;code&gt;/history&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Adding the Dashboard (Optional)&lt;/h2&gt;
&lt;p&gt;The bot is fully functional headless, but the &lt;strong&gt;Next.js dashboard&lt;/strong&gt; is where you see why it does everything. It’s a separate app (plus a Laravel API for auth and role-based access) served behind nginx. The full stack is: bot → Laravel API (php-fpm) → Next.js dashboard (PM2) → nginx reverse proxy with SSL. It’s more involved than the bot itself, so I’m keeping the deep detail in the repo’s deployment docs rather than bloating this post.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🛠️ &lt;strong&gt;Wanted:&lt;/strong&gt; a one-command &lt;code&gt;docker compose up&lt;/code&gt; for the whole stack is on the roadmap. If you’d enjoy building it, it’s the single highest-impact contribution right now — &lt;a href="https://github.com/dineshstack/crypto_bot/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; and let’s talk.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;What It Costs to Run&lt;/h2&gt;
&lt;p&gt;Nobody tells you this, and it’s the reason people abandon self-hosted bots. Realistic monthly cost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPS:&lt;/strong&gt; ~$5–$12/month for a 2 vCPU / 4&amp;nbsp;GB box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic API:&lt;/strong&gt; the 4-hour loop uses cheap Claude Haiku, so the base cost is low; the weekly deep reviews and any research/report generation use pricier models. Budget a small, predictable amount and watch it for the first week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else&lt;/strong&gt; (Binance testnet, CoinGecko free tier, Telegram): free.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The takeaway: cheap to run in testnet, but the Anthropic bill scales with how often you trigger the heavy AI features — so keep an eye on it.&lt;/p&gt;
&lt;h2&gt;Common Mistakes (Gotchas I Hit)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The update script doesn’t pull.&lt;/strong&gt; &lt;code&gt;deploy/update.sh&lt;/code&gt; reinstalls and restarts but doesn’t &lt;code&gt;git pull&lt;/code&gt; — run &lt;code&gt;git pull&lt;/code&gt; yourself first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel defaults to SQLite.&lt;/strong&gt; If you add the dashboard, its API must use &lt;code&gt;DB_CONNECTION=mysql&lt;/code&gt; pointed at the bot’s database, or the dashboard shows no data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the migrations and seeder.&lt;/strong&gt; The dashboard’s roles and admin user come from &lt;code&gt;php artisan migrate&lt;/code&gt; + &lt;code&gt;db:seed&lt;/code&gt; — skip it and login breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange minimums.&lt;/strong&gt; Binance rejects orders under a $5 minimum notional — the bot handles this now, but it’s the classic “why did my trade fail at $0” trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weekly review is quiet at first.&lt;/strong&gt; It stays dormant until there are 7 days of trades to review. That’s expected, not a bug.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;You’re Live — Now What?&lt;/h2&gt;
&lt;p&gt;You’ve deployed an AI crypto trading bot that analyses the market every four hours, explains its reasoning, and asks before it trades. Let it run on testnet for a couple of weeks and watch the &lt;code&gt;/performance&lt;/code&gt; numbers accumulate before you even think about real money.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;🎉 &lt;strong&gt;Got it running?&lt;/strong&gt; I’d love to see it — drop a comment or a screenshot. And if this guide saved you time:&lt;br&gt;⭐ &lt;a href="https://github.com/dineshstack/crypto_bot" rel="noopener noreferrer"&gt;&lt;strong&gt;Star the repo on GitHub&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;☕ &lt;a href="https://ko-fi.com/dineshstack" rel="noopener noreferrer"&gt;&lt;strong&gt;Support the project on Ko-fi&lt;/strong&gt;&lt;/a&gt; (it covers the API and server costs that keep it running)&lt;/p&gt;&lt;/blockquote&gt;
&lt;img src="/images/ai-crypto-trading-bot-telegram-deploy.png" alt="AI crypto trading bot deployed on a VPS sending an analysis message on Telegram"&gt;First contact: the deployed bot’s analysis message arriving in Telegram.
&lt;h3&gt;Related posts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dineshstack.com/en/ai-crypto-trading-bot-claude" rel="noopener noreferrer"&gt;Part 1 — How I Built an AI Crypto Trading Bot with Claude AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 3 — How to Contribute to the Project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; AI Crypto Trading Bot, VPS Deployment, Self-Hosting, Binance, DevOps&lt;/p&gt;
&lt;p&gt;Disclaimer: For educational and research purposes only. Not financial advice, not a solicitation to trade. Cryptocurrency trading carries substantial risk of loss. Always start on testnet and never trade money you can’t afford to lose.&lt;/p&gt;


</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>crypto</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Built an AI Crypto Trading Bot with Claude AI</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Fri, 24 Jul 2026 18:28:08 +0000</pubDate>
      <link>https://dev.to/dineshstack/how-i-built-an-ai-crypto-trading-bot-with-claude-ai-426e</link>
      <guid>https://dev.to/dineshstack/how-i-built-an-ai-crypto-trading-bot-with-claude-ai-426e</guid>
      <description>&lt;h1&gt;How I Built an AI Crypto Trading Bot with Claude AI&lt;/h1&gt;
&lt;p&gt;This is the definitive guide to how an &lt;strong&gt;AI crypto trading bot&lt;/strong&gt; built on Claude (Anthropic) actually works — the architecture, the design decisions, and the honest results — so you don’t have to piece it together from a dozen scattered tabs. It’s a real, running system: a multi-agent Claude pipeline for decisions, a machine-learning ensemble for a second opinion, layered risk controls, and a full Next.js dashboard. It trades BTC and ETH on Binance, is controlled from Telegram, and is &lt;a href="https://github.com/dineshstack/crypto_bot" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Part 1 of a 3-part series.&lt;/strong&gt; This post covers the architecture. Part 2 is a full deploy-to-your-VPS walkthrough, and Part 3 is about contributing. If it’s useful, a ⭐ on the repo genuinely helps.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;One honest note up front:&lt;/strong&gt; this is a research and educational project, currently running in paper-trading (Binance testnet). It is not a get-rich-quick bot and nothing here is financial advice. I’ll show you exactly what my backtests reveal later in the post — the engineering is solid, but “does it print money” is still an open question.&lt;/p&gt;
&lt;h2&gt;What Is an AI Crypto Trading Bot?&lt;/h2&gt;
&lt;p&gt;An &lt;strong&gt;AI crypto trading bot&lt;/strong&gt; is software that analyses the market and places trades automatically, using artificial intelligence to make the decisions a human trader normally would. The interesting question isn’t “can it place orders” — that’s trivial — it’s “can a large language model reason through a live, messy, adversarial market and act sensibly.” Crypto is about as messy and adversarial as markets get, which is exactly why it’s a good test.&lt;/p&gt;
&lt;p&gt;Rather than one giant “should I buy?” prompt, this bot orchestrates several specialised components: three Claude agents, a quant ML model, and a strict risk-management layer that has the final say before any money moves. Here’s how they fit together.&lt;/p&gt;
&lt;h2&gt;How the AI Trading Bot Works: The 4-Hour Cycle&lt;/h2&gt;
&lt;p&gt;The simplest way to understand the system is to follow one cycle. Every four hours — and immediately if a flash crash is detected — the bot runs this loop:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Market data ─┐&lt;br&gt;
News/social ─┼─▶  3 Claude agents  ─┐&lt;br&gt;
On-chain ────┤   (Market · Sentiment · Decision)&lt;br&gt;
Derivatives ─┘                       ├─▶ Decision ─▶ Risk sizing ─▶ Binance&lt;br&gt;
Historical ──────▶ ML ensemble ──────┘        │&lt;br&gt;
                                              └─▶ MySQL ─▶ Dashboard + Telegram&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In words, each cycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Gathers the market state — 20+ technical indicators, derivatives data (funding rate, open interest, long/short ratio), the Fear &amp;amp; Greed index, and multi-timeframe trend agreement.&lt;/li&gt;
&lt;li&gt;Grades its last decision against what the market actually did, and turns mistakes into one-line lessons.&lt;/li&gt;
&lt;li&gt;Checks a set of safety circuit breakers that can pause trading or shrink positions.&lt;/li&gt;
&lt;li&gt;Asks three specialised Claude agents what to do.&lt;/li&gt;
&lt;li&gt;Gets a second opinion from an ML model trained on thousands of historical candles.&lt;/li&gt;
&lt;li&gt;Sizes the trade with risk-managed position sizing and executes on Binance with a protective stop-loss (after Telegram approval, in live mode).&lt;/li&gt;
&lt;li&gt;Logs everything — every prompt, response, and number — to a database that feeds the dashboard and a Telegram message.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Multi-Agent Claude Pipeline&lt;/h2&gt;
&lt;p&gt;The obvious way to use an LLM for trading is one mega-prompt: “here’s everything, tell me buy/sell/hold.” It works, but the reasoning gets shallow — the model tries to hold every consideration at once. So the bot runs &lt;strong&gt;three specialised Claude agents&lt;/strong&gt; instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Market Analyst&lt;/strong&gt; — sees only the chart and the numbers: RSI, MACD, Bollinger Bands, ATR, VWAP, Ichimoku, funding rate, open interest, multi-timeframe regime. A pure technical read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analyst&lt;/strong&gt; — sees only the context: news, social sentiment, on-chain flows, options positioning (put/call, max pain), whale transactions, and macro (DXY, S&amp;amp;P, gold, VIX). It runs in parallel with the Market Analyst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision Maker&lt;/strong&gt; — sees both assessments, plus the ML prediction, the portfolio, and lessons from past mistakes. It has hard rules baked in (won’t over-allocate, won’t panic-sell an uptrend, defaults to HOLD when signals conflict) and produces the final decision as structured JSON:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{&lt;br&gt;
  "action": "buy",&lt;br&gt;
  "confidence": 0.72,&lt;br&gt;
  "trade_usd": 6.0,&lt;br&gt;
  "risk": "medium",&lt;br&gt;
  "reason": "RSI deeply oversold + OBV rising divergence + bullish options&lt;br&gt;
             positioning create an asymmetric reversal setup; macro is a&lt;br&gt;
             headwind but allocation is low, so a small buy is justified.",&lt;br&gt;
  "signals": ["rsi_oversold", "obv_divergence", "options_bullish"]&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Splitting the problem this way produces better, more legible decisions — each agent focuses, and the final synthesis weighs two clean expert opinions instead of one blurry one. Every call is logged in full, so the dashboard can show the entire chain of reasoning behind any trade. For the fast 4-hour loop I use &lt;strong&gt;Claude Haiku&lt;/strong&gt;; for heavier reasoning (weekly reviews, deep research, reports) I use &lt;strong&gt;Claude Fable 5&lt;/strong&gt; with an automatic fallback to &lt;strong&gt;Claude Opus 4.8&lt;/strong&gt;.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fik.imagekit.io%2Fu7tvtcis2%2Fscheduler%2Fgallery%2Fai-crypto-trading-bot-claude-dashboard_1784913685_wNs61pInY.png" width="800" height="385"&gt;&lt;h2&gt;The Machine Learning Ensemble: A Quant Second Opinion&lt;/h2&gt;
&lt;p&gt;The Claude agents make the call, but they don’t get the last word alone. Running alongside them is a classic quant model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;stacking ensemble&lt;/strong&gt; — XGBoost and LightGBM base learners feeding a logistic-regression meta-learner.&lt;/li&gt;
&lt;li&gt;Trained on 5,000+ candles across three timeframes (1h, 4h, 1d), with 60+ engineered features.&lt;/li&gt;
&lt;li&gt;Labelled with the &lt;strong&gt;triple-barrier method&lt;/strong&gt; (target / stop / timeout) from Advances in Financial Machine Learning.&lt;/li&gt;
&lt;li&gt;Validated with &lt;strong&gt;purged walk-forward cross-validation&lt;/strong&gt; — the honest way to test a time-series model, which stops the future leaking into the past.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A Hidden Markov Model also classifies the market into regimes (strong trend, weak trend, range, high volatility, crash) so the strategy can adapt. The Decision agent treats the ML prediction as one more expert at the table — sometimes it agrees, sometimes it overrides. Keeping them separate means neither a hallucinating LLM nor an overfit model can unilaterally move money.&lt;/p&gt;
&lt;h2&gt;Risk Management Is the Real Product&lt;/h2&gt;
&lt;p&gt;Here’s something I believe strongly: a 55%-accurate model with good risk management beats a 65%-accurate model with bad sizing. So most of the engineering went here, not into the “AI” headline. The bot uses quarter-Kelly position sizing, ATR-based stops that widen in losing streaks, protective stop-loss orders resting on the exchange, human approval for every live trade via Telegram, and five circuit breakers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Circuit-breaker thresholds (config.py)&lt;br&gt;
DAILY_LOSS_HALT_PCT   = 0.03   # 3% intraday loss -&amp;gt; pause for the day&lt;br&gt;
DRAWDOWN_REDUCE_PCT   = 0.10   # 10% drawdown     -&amp;gt; halve position sizes&lt;br&gt;
DRAWDOWN_HALT_PCT     = 0.20   # 20% drawdown     -&amp;gt; halt all trading&lt;br&gt;
CONSECUTIVE_LOSS_HALT = 5      # 5 losses in a row -&amp;gt; pause

&lt;/code&gt;&lt;h1&gt;&lt;code&gt;&lt;br&gt;
  &lt;br&gt;
  

&lt;ul&gt;
&lt;li&gt;an equity moving-average filter that throttles sizing in a downtrend&lt;/li&gt;
&lt;/ul&gt;&lt;/code&gt;&lt;/h1&gt;&lt;/pre&gt;
&lt;p&gt;The philosophy: it should be very hard for the bot to lose a lot quickly, even when the AI is wrong — because sometimes it will be.&lt;/p&gt;
&lt;h2&gt;Common Mistakes When Building an AI Trading Bot&lt;/h2&gt;
&lt;p&gt;Things I got wrong so you don’t have to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the LLM’s suggested trade size.&lt;/strong&gt; The model would suggest a dollar amount that the risk layer then overrode — and for a while the two disagreed silently. Let one component own sizing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring exchange minimums.&lt;/strong&gt; Binance rejects orders below a $5 minimum notional. Risk-managed sizing quietly produced sub-$5 orders that failed at $0 — roughly half the buy signals were lost before I caught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measuring “correct/wrong” on too short a window.&lt;/strong&gt; A ±2% move in 4 hours almost never happens in a range-bound market, so every trade scored “neutral” and the win rate was undefined. Measure decisions by real profit-and-loss over a sensible horizon instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backtesting without a leakage check.&lt;/strong&gt; A backtest that looks amazing is usually cheating. Always run a placebo test (below).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Tech Stack&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Claude Haiku 4.5 (fast analysis), Claude Fable 5 + Opus 4.8 (deep reasoning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine learning&lt;/td&gt;
&lt;td&gt;XGBoost + LightGBM stacking ensemble, Optuna tuning, HMM regime detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trading&lt;/td&gt;
&lt;td&gt;Binance Spot API via CCXT, WebSocket for real-time data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Python 3.12 (bot), Laravel + FastAPI (APIs), MySQL 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 16, React 19, Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control &amp;amp; infra&lt;/td&gt;
&lt;td&gt;Telegram Bot API, Ubuntu VPS, systemd, nginx, PM2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Honest Results: What the Backtests Actually Show&lt;/h2&gt;
&lt;p&gt;This is the section most “I built a trading bot” posts leave out, and it’s the most important. I tested the strategy over 24 months with fees and slippage modelled, plus a &lt;strong&gt;placebo/leakage test&lt;/strong&gt; — feed the model a fake, time-shifted signal; if it still “works,” your harness is cheating. The findings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No leakage.&lt;/strong&gt; Under the placebo, the edge collapses — so the good results come from real signal, not a look-ahead bug. This is the check most backtests fail silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It survives out-of-sample.&lt;/strong&gt; On data the model never trained on, across multiple market windows, it stayed positive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But it’s a defensive, market-neutral scalper.&lt;/strong&gt; It sidesteps crashes well (up while BTC fell 27%) but underperforms simple buy-and-hold in a bull market, and its absolute returns are small.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In plain terms: it’s a well-engineered platform that protects capital and trades with discipline — but whether it has meaningful, tradeable alpha is still an open research question. I’d rather tell you that than show a cherry-picked equity curve. It’s also exactly where contributions are most valuable.&lt;/p&gt;
&lt;h2&gt;Conclusion: Get the Code and Build With Me&lt;/h2&gt;
&lt;p&gt;An AI crypto trading bot is a genuinely fun way to combine LLM reasoning, machine learning, and real-world execution into one system — and building it in the open means the interesting problems (chiefly, “is there real alpha here?”) are shared. The entire project is MIT-licensed on GitHub:&lt;/p&gt;
&lt;p&gt;👉 &lt;a href="https://github.com/dineshstack/crypto_bot" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/dineshstack/crypto_bot&lt;/strong&gt;&lt;/a&gt; — ⭐ star it if this was useful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Want to deploy your own? That’s Part 2: Deploy an AI Crypto Trading Bot to Your VPS — with a real cost breakdown and every gotcha.&lt;/li&gt;
&lt;li&gt;Want to contribute? See Part 3: How to Contribute and the repo’s &lt;a href="https://github.com/dineshstack/crypto_bot/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="/images/ai-crypto-trading-bot-claude-dashboard.png" alt="AI crypto trading bot dashboard built with Claude AI showing the three-agent reasoning and risk data"&gt;The dashboard: click any trade to see the full three-agent reasoning, ML prediction, and stop-loss levels.
&lt;h3&gt;Related posts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Part 2 — Deploy an AI Crypto Trading Bot to Your Own VPS&lt;/li&gt;
&lt;li&gt;Part 3 — How to Contribute to the Project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; AI Crypto Trading Bot, Claude AI, Machine Learning, Algorithmic Trading, Open Source&lt;/p&gt;
&lt;p&gt;Disclaimer: This project is for educational and research purposes only. It is not financial advice and not a solicitation to trade. Cryptocurrency trading carries substantial risk of loss. Never trade with money you can’t afford to lose, and always start on testnet.&lt;/p&gt;





&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/ai-crypto-trading-bot-claude?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>claude</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Higgsfield Credits vs Unlimited: When to Use Which?</title>
      <dc:creator>Dinesh Wijethunga</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:57:49 +0000</pubDate>
      <link>https://dev.to/dineshstack/higgsfield-credits-vs-unlimited-when-to-use-which-39id</link>
      <guid>https://dev.to/dineshstack/higgsfield-credits-vs-unlimited-when-to-use-which-39id</guid>
      <description>&lt;p&gt;Both Higgsfield's &lt;strong&gt;credit&lt;/strong&gt; plans and its &lt;strong&gt;unlimited&lt;/strong&gt; mode let you generate — but they are not interchangeable, and confusing them will either waste money or break your automation. Here's how to pick, with the real per-clip costs I measured.&lt;/p&gt;
&lt;p&gt;This is post #4 of the &lt;a href="/en/automate-ai-video-creation-claude-higgsfield"&gt;AI Video Automation series&lt;/a&gt;. It's the lesson that cost me the most time, so I put it in its own post.&lt;/p&gt;
&lt;h2&gt;Overview: two different systems&lt;/h2&gt;
&lt;p&gt;Higgsfield markets "unlimited generations on all top models." That is true — &lt;strong&gt;in the web UI&lt;/strong&gt;. It is not true for the CLI. Their own pricing page states it directly:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Unlimited models and Free Generations are accessible only via higgsfield.ai and are not accessible on MCP/CLI, Canvas or Supercomputer.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I confirmed it empirically: one Nano Banana Pro image via the CLI dropped my balance from 10 credits to 8. The web UI's unlimited "relaxed" queue and the CLI's credit-metered queue are two separate systems.&lt;/p&gt;
&lt;h2&gt;Key differences&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Web UI (unlimited)&lt;/th&gt;
&lt;th&gt;CLI / API (credits)&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free on an unlimited plan/trial&lt;/td&gt;
&lt;td&gt;Credits per generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;Manual, one at a time&lt;/td&gt;
&lt;td&gt;Fully scriptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Relaxed (slow) queue&lt;/td&gt;
&lt;td&gt;Priority queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Free manual exploration&lt;/td&gt;
&lt;td&gt;Hands-off batch pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;The real CLI costs (measured)&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 pro, 5s video&lt;/td&gt;
&lt;td&gt;12.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 std, 5s video&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0 std, 3s video&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 2.0, 1080p 5s&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 2.0, 720p 5s&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nano Banana Pro image, 2K&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Always check before a batch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;higgsfield generate cost seedance_2_0 --duration 5 --resolution 1080p --prompt "test"

&lt;h1&gt;
  
  
  45 credits — drop to 720p and it's 22.5
&lt;/h1&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h2&gt;When to use the web UI (unlimited)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You're exploring, testing quality, or making a handful of clips.&lt;/li&gt;
&lt;li&gt;You have an unlimited plan or an active trial and don't mind clicking each generation.&lt;/li&gt;
&lt;li&gt;Budget is zero and you have time to babysit the relaxed queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to use the CLI (credits)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You're running a &lt;strong&gt;batch pipeline&lt;/strong&gt; — a whole library, hands-off (see &lt;a href="/en/higgsfield-python-batch-queue-runner"&gt;post #5&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;You want reproducibility, retries, and overnight runs.&lt;/li&gt;
&lt;li&gt;You value time over the credit cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Verdict / plan sizing&lt;/h2&gt;
&lt;p&gt;A ~46-shot cinematic library costs about &lt;strong&gt;750–1,000 credits&lt;/strong&gt; for one take each (Kling pro for heroes, Seedance 720p for backgrounds, cheap images). So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PLUS (1,200 credits)&lt;/strong&gt; covers one full library pass plus ~35 regenerations — the right buy for a single project. Bill it monthly and cancel if it's one-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ULTRA (3,000 credits)&lt;/strong&gt; only if you want 2–3 full takes of everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free route:&lt;/strong&gt; do it manually in the web UI on a trial — unlimited but slow and hands-on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rule of thumb: explore free in the web UI, then pay for credits only when you automate.&lt;/p&gt;
&lt;h2&gt;Output / preview&lt;/h2&gt;
&lt;p&gt;Now that the cost model is clear, the next post automates the whole thing within a credit budget.&lt;/p&gt;
&lt;p&gt;[Insert preview: the cost comparison table — filename &lt;code&gt;higgsfield-credits-vs-unlimited.png&lt;/code&gt;]&lt;/p&gt;
&lt;h2&gt;Related posts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="/en/cinematic-ai-video-prompts-formula"&gt;Cinematic AI Video Prompts: A Step-by-Step Formula&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="/en/higgsfield-python-batch-queue-runner"&gt;Automate Higgsfield with a Python Batch Queue Runner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Series hub: &lt;a href="/en/automate-ai-video-creation-claude-higgsfield"&gt;AI Video Automation overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dineshstack.com/en/higgsfield-credits-vs-unlimited?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;dineshstack.com&lt;/a&gt; — read the full version with code samples and updates there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>video</category>
      <category>higgsfield</category>
      <category>pricing</category>
    </item>
  </channel>
</rss>
