<?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: Roman Shalabanov</title>
    <description>The latest articles on DEV Community by Roman Shalabanov (@roman_shalabanov_e53b30b6).</description>
    <link>https://dev.to/roman_shalabanov_e53b30b6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3471171%2F44cfdd94-b74a-4799-af93-f8068b81758e.jpg</url>
      <title>DEV Community: Roman Shalabanov</title>
      <link>https://dev.to/roman_shalabanov_e53b30b6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roman_shalabanov_e53b30b6"/>
    <language>en</language>
    <item>
      <title>I Built an AI Employee That Monitors My SaaS Store 24/7 — Here's the Architecture</title>
      <dc:creator>Roman Shalabanov</dc:creator>
      <pubDate>Tue, 31 Mar 2026 18:52:26 +0000</pubDate>
      <link>https://dev.to/roman_shalabanov_e53b30b6/i-built-an-ai-employee-that-monitors-my-saas-store-247-heres-the-architecture-pm7</link>
      <guid>https://dev.to/roman_shalabanov_e53b30b6/i-built-an-ai-employee-that-monitors-my-saas-store-247-heres-the-architecture-pm7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;What if your SaaS store had a full-time operations worker that never sleeps, catches every failed payment within minutes, and answers "how many customers paid today?" in Telegram — at nearly zero cost per question?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what I built. Four modular Laravel packages, a deterministic API simulator, a two-tier AI intent classifier, a heartbeat monitoring engine with proactive workflows, and integrations with Telegram, OpenClaw, Slack, and plain HTTP.&lt;/p&gt;

&lt;p&gt;This article walks through the architecture, the reasoning behind every layer, and the real problems I solved building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/romansh/laravel-creem-agent-demo/4ff35a72d068acf453f97d8eb2865c8935646b39/docs/img/pic0.png" rel="noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fromansh%2Flaravel-creem-agent-demo%2F4ff35a72d068acf453f97d8eb2865c8935646b39%2Fdocs%2Fimg%2Fpic0.png" width="800" height="600"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system is composed of four independent Composer packages that snap together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;laravel-creem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SDK — HTTP client, webhook verification, event-driven architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;laravel-creem-cli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Artisan CLI — mirrors the native &lt;code&gt;creem&lt;/code&gt; CLI, works standalone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;laravel-creem-agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent — chat, heartbeat engine, proactive workflows, notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;creem-simulator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full Creem API mock — deterministic seeding, webhook loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every package is a standalone Composer library. You can use &lt;code&gt;laravel-creem&lt;/code&gt; without the agent and the CLI without OpenClaw. But when they're combined, you get something powerful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Four Packages Instead of One?
&lt;/h2&gt;

&lt;p&gt;Because real SaaS systems are composed of layers, not monoliths.&lt;/p&gt;

&lt;p&gt;A developer who only needs the Creem SDK shouldn't install a Telegram bot. A DevOps engineer who wants CLI access for scripts shouldn't need an AI classifier. And someone building a custom dashboard can use the SDK + CLI without ever touching the agent.&lt;/p&gt;

&lt;p&gt;The packages declare soft dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;laravel-creem-agent
  ├── requires: laravel-creem (SDK)
  └── suggests: laravel-creem-cli (for native CLI acceleration)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent auto-detects whether the native &lt;code&gt;creem&lt;/code&gt; CLI binary is installed. If yes, it uses shell exec for speed. If not, it falls back to in-process SDK calls — same result, zero manual configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two-Tier Intent Classifier
&lt;/h2&gt;

&lt;p&gt;Every user message goes through a two-stage parsing pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User message
    ↓
CommandParser (regex rules — zero cost)
    ↓ if intent == unknown
LlmCommandParser (AI call — low cost)
    ↓ if still unknown
"I didn't understand that. Type 'help' for options."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 1: Rule-Based Parser — Free
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;CommandParser&lt;/code&gt; matches common phrases with regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="s2"&gt;"how many active subscriptions?"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;intent:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query_subscriptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;status:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;active&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"any payment issues?"&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;intent:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query_subscriptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;status:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;past_due&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"run heartbeat"&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;intent:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;run_heartbeat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"cancel sub_abc123"&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;intent:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancel_subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sub_abc&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard phrases like &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;help&lt;/code&gt;, &lt;code&gt;recent transactions&lt;/code&gt;, &lt;code&gt;products&lt;/code&gt;, &lt;code&gt;how many customers&lt;/code&gt; — all handled here. No API call, no tokens, no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: LLM Fallback — Only When Needed
&lt;/h3&gt;

&lt;p&gt;Free-form questions like &lt;em&gt;"how's the store doing?"&lt;/em&gt; or &lt;em&gt;"what's going on with payments?"&lt;/em&gt; don't match any regex. The agent sends a prompt to an LLM and gets back structured JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Model Selection Matters — A Lot
&lt;/h3&gt;

&lt;p&gt;This was one of the biggest lessons from the project.&lt;/p&gt;

&lt;p&gt;I started with &lt;strong&gt;gpt-4o-mini&lt;/strong&gt; as the OpenClaw skill model. It was fast and... completely ignored the skill instructions. When the &lt;code&gt;SKILL.md&lt;/code&gt; file explicitly said &lt;em&gt;"call the Laravel endpoint via curl"&lt;/em&gt;, gpt-4o-mini would instead answer from its own knowledge: &lt;em&gt;"I don't have transaction data loaded in this workspace yet. Where are your transactions stored?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It literally refused to use the tool it was given.&lt;/p&gt;

&lt;p&gt;Switching to &lt;strong&gt;gpt-5.4&lt;/strong&gt; fixed everything instantly. The model read the skill instructions, called curl, forwarded the exact user question, and relayed the response. Night and day difference.&lt;/p&gt;

&lt;p&gt;Then I tested the middle ground:&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;Accuracy&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;Poor — ignores skill instructions&lt;/td&gt;
&lt;td&gt;✗ Not viable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.4-nano&lt;/td&gt;
&lt;td&gt;Mediocre — sometimes simplifies questions&lt;/td&gt;
&lt;td&gt;Acceptable for simple queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gpt-5.4-mini&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Good — follows instructions reliably&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;★ Best balance&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.4&lt;/td&gt;
&lt;td&gt;Excellent — perfect routing&lt;/td&gt;
&lt;td&gt;Overkill for intent classification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sweet spot is &lt;strong&gt;gpt-5.4-mini&lt;/strong&gt;: it follows routing instructions and preserves user intent qualifiers (&lt;code&gt;how many&lt;/code&gt;, &lt;code&gt;today&lt;/code&gt;, &lt;code&gt;successful&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; if your AI agent seems broken, check the model first. The difference between a smaller and a larger model can be the difference between "works" and "completely useless."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Heartbeat Engine
&lt;/h2&gt;

&lt;p&gt;The heartbeat is the core monitoring loop. It runs on a schedule (configurable per store) and detects &lt;em&gt;what changed&lt;/em&gt; since the last check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Load previous state from disk
2. Query current metrics:
   ├─ TransactionChecker  → new sales, revenue
   ├─ SubscriptionChecker → status transitions
   └─ CustomerChecker     → growth
3. ChangeDetector → compute deltas
4. Classify severity: good_news | warning | alert
5. Persist new state
6. Fire events → trigger workflows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  State Persistence
&lt;/h3&gt;

&lt;p&gt;Each store gets a JSON state file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastCheckAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-30T14:22:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastTransactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"txn_abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactionCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;487&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"trialing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"past_due"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"canceled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent compares current API data against this snapshot and surfaces only the differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proactive Workflows
&lt;/h3&gt;

&lt;p&gt;Workflows listen to heartbeat events and take autonomous action:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failed Payment Recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;subscription → &lt;code&gt;past_due&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Alert via Telegram/Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Churn Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≥2 cancellations in one cycle&lt;/td&gt;
&lt;td&gt;Immediate alert with details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Revenue Digest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scheduled (daily)&lt;/td&gt;
&lt;td&gt;Summary of sales and growth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Anomaly Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unusual metric drops&lt;/td&gt;
&lt;td&gt;Flag for investigation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each workflow dispatches Laravel notifications through configurable channels — Telegram, Slack, email, or database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Store Support
&lt;/h2&gt;

&lt;p&gt;Real businesses run multiple stores or product lines. The agent handles this natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/creem-agent.php&lt;/span&gt;
&lt;span class="s1"&gt;'stores'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'profile'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'heartbeat_frequency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// hours&lt;/span&gt;
        &lt;span class="s1"&gt;'notifications'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'telegram'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'enterprise'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'profile'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'heartbeat_frequency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'notifications'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'slack'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In chat: &lt;em&gt;"switch to store enterprise"&lt;/em&gt; → all subsequent queries use the enterprise profile.&lt;/p&gt;

&lt;p&gt;In heartbeat: &lt;code&gt;php artisan creem-agent:heartbeat --all-stores&lt;/code&gt; → each store checked independently with its own state file and notification channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Creem Simulator
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcec6d4efpx06mvnq50zu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcec6d4efpx06mvnq50zu.png" alt="Simulator: deterministic seeding with different configurations" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simulator is a full standalone Laravel app that implements the entire Creem API surface. It runs as a Docker service alongside the main app and lets you test everything without a real payment processor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deterministic Seeding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan simulator:seed-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--products&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 &lt;span class="nt"&gt;--customers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;40 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subscriptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24 &lt;span class="nt"&gt;--transactions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45 &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a realistic baseline dataset. Same seed, same data — every time. Perfect for CI/CD pipelines and reproducible demos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario Advancement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan simulator:advance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sales&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;--new-customers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--past-due&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="nt"&gt;--cancellations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--send-webhooks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command generates &lt;em&gt;exactly&lt;/em&gt; the changes you specify. Three new sales, two new customers, two subscriptions going past-due, one cancellation. The &lt;code&gt;--send-webhooks&lt;/code&gt; flag immediately fires signed webhook events back to the agent app — triggering the full notification pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8gfxzpybkkxusw3w8yqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8gfxzpybkkxusw3w8yqd.png" alt="Simulator: advance scenario with webhook delivery" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnokg4ag716gq1spv03el.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnokg4ag716gq1spv03el.png" alt="Telegram: real-time new sale alert triggered by simulator webhook" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the scenario gets more complex — multiple sales, cancellations, and past-due transitions — the agent handles each event independently:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz93awerhhtq1laonpp4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz93awerhhtq1laonpp4v.png" alt="Simulator: multiple advance commands generating sales, cancellations, and past-due events" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8ev4mh4xrr0sgbip93g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8ev4mh4xrr0sgbip93g.png" alt="Telegram: batch of alerts — new sales and subscription cancellations arriving in real time" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook Loopback
&lt;/h3&gt;

&lt;p&gt;The simulator signs webhooks with the same HMAC-SHA256 algorithm as the real Creem API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;simulator:advance &lt;span class="nt"&gt;--send-webhooks&lt;/span&gt;
  → generates checkout.completed event
  → POST http://app/creem/webhook
    Headers: &lt;span class="o"&gt;{&lt;/span&gt;creem-signature: hmac-sha256&lt;span class="o"&gt;(&lt;/span&gt;payload, secret&lt;span class="o"&gt;)}&lt;/span&gt;
  → App receives → VerifyCreemWebhook middleware validates
  → WebhookController dispatches CheckoutCompleted event
  → TelegramNotifier sends &lt;span class="s2"&gt;"✅ New sale: Product (&lt;/span&gt;&lt;span class="nv"&gt;$10&lt;/span&gt;&lt;span class="s2"&gt;.00)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent doesn't know (or care) whether the webhook came from the simulator or from Creem production. The signature is valid, the payload is structured correctly, and the workflows fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;Without a simulator, testing a payment monitoring agent means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating real test transactions on a payment platform&lt;/li&gt;
&lt;li&gt;Waiting for webhook delivery&lt;/li&gt;
&lt;li&gt;Hoping the timing works for your demo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With the simulator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Seed data in 2 seconds&lt;/li&gt;
&lt;li&gt;Advance the scenario with exact parameters&lt;/li&gt;
&lt;li&gt;Get immediate, deterministic results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the difference between a demo that &lt;em&gt;might&lt;/em&gt; work and a demo that works every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talking to the Agent: curl Examples
&lt;/h2&gt;

&lt;p&gt;The agent exposes a simple HTTP endpoint. No SDK required — just curl:&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/creem-agent/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"status","source":"api"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Store 'default' — 28 active subscriptions, 52 customers, 487 transactions. Last heartbeat: 14 minutes ago. No alerts."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subscription Query
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/creem-agent/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"any payment issues?","source":"api"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"⚠️ 2 subscription(s) are past due in store 'default':&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;• sub_abc123 — $29.99/mo (past due since Mar 28)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;• sub_def456 — $9.99/mo (past due since Mar 30)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running Heartbeat via Chat
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/creem-agent/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"run heartbeat","source":"api"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Heartbeat complete — 5 change(s) detected:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✅ 3 new transaction(s) — $89.97 revenue&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;⚠️ sub_ghi789 transitioned to past_due&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;🔴 sub_jkl012 was canceled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffitev7re1ho3upv1v041.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffitev7re1ho3upv1v041.png" alt="Terminal: curl requests to the chat endpoint — status and payment issues" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Telegram Integration
&lt;/h2&gt;

&lt;p&gt;The agent supports two Telegram paths. In both cases, you talk to the bot using natural language — ask about store status, payment issues, or trigger a heartbeat check:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wtl4jyfoud15s1ijqzq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wtl4jyfoud15s1ijqzq.png" alt="Telegram: natural-language conversation — store status and payment issue queries" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: Direct Laravel Webhook
&lt;/h3&gt;

&lt;p&gt;The agent runs its own webhook endpoint. Telegram messages come directly to the Laravel app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;User → Telegram Bot API → ngrok → POST /creem-agent/telegram/webhook
  → AgentManager → parse → route → respond
  → POST https://api.telegram.org/sendMessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the simplest setup. No external tools. Just a bot token, an ngrok tunnel, and the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 2: OpenClaw-Powered Telegram
&lt;/h3&gt;

&lt;p&gt;For teams already using OpenClaw, the agent publishes as an OpenClaw skill. OpenClaw handles Telegram natively, and the skill bridges messages to the Laravel endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;User → Telegram → OpenClaw Gateway → Skill &lt;span class="o"&gt;(&lt;/span&gt;SKILL.md&lt;span class="o"&gt;)&lt;/span&gt;
  → curl POST http://localhost:8000/creem-agent/chat
  → Response relayed back through OpenClaw → Telegram
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill is a single &lt;code&gt;SKILL.md&lt;/code&gt; file — no shell scripts, no binaries. Install it from ClawHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openclaw-laravel-creem-agent-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Support Both?
&lt;/h3&gt;

&lt;p&gt;Because OpenClaw adds value beyond basic messaging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-skill orchestration&lt;/strong&gt; — the agent becomes one skill among many&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session management&lt;/strong&gt; — OpenClaw handles conversation state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway security&lt;/strong&gt; — shared secrets, pairing approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel flexibility&lt;/strong&gt; — same skill works on Telegram, WebChat, Discord&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if you don't use OpenClaw, the agent works perfectly fine on its own. No vendor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI Package
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;laravel-creem-cli&lt;/code&gt; wraps the Creem API into Artisan commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan creem:subscriptions list &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="nt"&gt;--json&lt;/span&gt;
php artisan creem:transactions list &lt;span class="nt"&gt;--json&lt;/span&gt;
php artisan creem:customers list &lt;span class="nt"&gt;--json&lt;/span&gt;
php artisan creem:products list &lt;span class="nt"&gt;--json&lt;/span&gt;
php artisan creem:whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's designed as a standalone package. If the native &lt;code&gt;creem&lt;/code&gt; CLI binary is installed, the agent uses it for speed. If not, &lt;code&gt;laravel-creem-cli&lt;/code&gt; handles everything through the SDK.&lt;/p&gt;

&lt;p&gt;This dual-driver architecture means zero configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent needs subscription data
  ↓
CreemCliManager:
  1. Check: is native `creem` binary available? (cached 24h)
  2. If yes → NativeCliDriver: shell exec `creem subscriptions list --json`
  3. If no  → ArtisanCliDriver: Creem::profile()-&amp;gt;subscriptions()-&amp;gt;list()
  ↓
Same JSON result either way
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Topology
&lt;/h2&gt;

&lt;p&gt;The demo stack runs five services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# Laravel Octane + FrankenPHP (port 8000)&lt;/span&gt;
  &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# Queue worker for async jobs&lt;/span&gt;
  &lt;span class="na"&gt;scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Cron runner for heartbeat schedule&lt;/span&gt;
  &lt;span class="na"&gt;simulator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Mock Creem API (internal only)&lt;/span&gt;
  &lt;span class="na"&gt;ngrok&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# Public tunnel for webhooks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkru9iqfo4fs9vwhh3o8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkru9iqfo4fs9vwhh3o8p.png" alt="Docker: all five containers running — app, queue, scheduler, simulator, ngrok" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simulator is only accessible inside the Docker network — it's not exposed to the host. The app talks to it via the Docker service name (&lt;code&gt;http://simulator/api/v1&lt;/code&gt;), and the same app config seamlessly switches to &lt;code&gt;https://api.creem.io/v1&lt;/code&gt; in production by changing one environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes This Different
&lt;/h2&gt;

&lt;p&gt;This isn't a script that calls an API and prints results. It's a &lt;strong&gt;platform&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Modular by design&lt;/strong&gt; — use any package independently or compose them together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-tier AI&lt;/strong&gt; — rule parser handles 80% of queries at zero cost; LLM catches everything else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proactive, not reactive&lt;/strong&gt; — heartbeat detects problems before users report them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulator-first development&lt;/strong&gt; — deterministic testing without real payment infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-store native&lt;/strong&gt; — manage multiple stores with independent configs and notification channels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel-agnostic&lt;/strong&gt; — Telegram, OpenClaw, Slack, HTTP, CLI — same agent, same logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-grade webhooks&lt;/strong&gt; — HMAC signature verification, retry handling, event-driven architecture&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Screenshot Reference
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8rywi1anjiwm2nkfrah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8rywi1anjiwm2nkfrah.png" alt="Figure 1: System architecture" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 1:&lt;/strong&gt; Full system architecture — all packages, data flows, and integration points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65g0x53d0i2mfpdf6mfz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65g0x53d0i2mfpdf6mfz.png" alt="Figure 2: Simulator seeding" width="800" height="487"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 2:&lt;/strong&gt; Creem Simulator deterministic seeding. Parameters control exact data volumes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2z8587ly0j8mmpv61b6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2z8587ly0j8mmpv61b6k.png" alt="Figure 3: Simulator advance with webhooks" width="800" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 3:&lt;/strong&gt; Scenario advancement with &lt;code&gt;--send-webhooks&lt;/code&gt;. Signed webhook delivered and processed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fim3mojkt0neknth914fc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fim3mojkt0neknth914fc.png" alt="Figure 4: Telegram — first sale alert" width="800" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 4:&lt;/strong&gt; Real-time Telegram alert triggered by the simulator webhook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yayyvpx2zie678foaxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yayyvpx2zie678foaxl.png" alt="Figure 5: Multiple events — sales, cancellations, past-due" width="800" height="548"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 5:&lt;/strong&gt; Multiple advance commands generating sales, cancellations, and past-due transitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzselmu8xidloyyjrm23l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzselmu8xidloyyjrm23l.png" alt="Figure 6: Telegram — batch of alerts" width="800" height="589"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 6:&lt;/strong&gt; Telegram receiving 3 new sale alerts and 2 subscription cancellation alerts in real time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flat5lc40ivqccgq16u53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flat5lc40ivqccgq16u53.png" alt="Figure 7: curl requests" width="800" height="189"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 7:&lt;/strong&gt; Querying the agent via curl — status overview and payment issue check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fon4uy1u5gqqed3tgh1q5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fon4uy1u5gqqed3tgh1q5.png" alt="Figure 8: Telegram — natural language queries" width="800" height="347"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 8:&lt;/strong&gt; Natural-language conversation in Telegram. The agent understands "how's the store doing?" and "are there any issues?" — same flow for direct webhook or OpenClaw path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa327piw19v4nk7wi2xa7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa327piw19v4nk7wi2xa7.png" alt="Figure 9: Docker topology" width="800" height="319"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 9:&lt;/strong&gt; Docker Compose topology — five services running: app, queue, scheduler, simulator, ngrok.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd3guy47bcnmr7ogq5sc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd3guy47bcnmr7ogq5sc.png" alt="Figure 10: Heartbeat via Telegram" width="800" height="319"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 10:&lt;/strong&gt; Heartbeat triggered via natural language in Telegram — "run it again" produces a full change report with new sales and customer counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The full source is on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/romansh/laravel-creem" rel="noopener noreferrer"&gt;laravel-creem&lt;/a&gt; — SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/romansh/laravel-creem-agent" rel="noopener noreferrer"&gt;laravel-creem-agent&lt;/a&gt; — Agent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/romansh/laravel-creem-agent-demo" rel="noopener noreferrer"&gt;laravel-creem-agent-demo&lt;/a&gt; — Demo + Simulator&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/romansh/laravel-creem-cli" rel="noopener noreferrer"&gt;laravel-creem-cli&lt;/a&gt; — CLI&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clawhub.com/skills/openclaw-laravel-creem-agent-skill" rel="noopener noreferrer"&gt;OpenClaw Skill on ClawHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simulator means you can test the entire system locally without any Creem API keys. Seed data, advance the scenario, watch the agent react.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>saas</category>
      <category>php</category>
    </item>
    <item>
      <title>LocalHelp: A Map Where Neighbors Help Each Other in One Click [DEV Weekend Challenge: Community]</title>
      <dc:creator>Roman Shalabanov</dc:creator>
      <pubDate>Mon, 02 Mar 2026 01:17:22 +0000</pubDate>
      <link>https://dev.to/roman_shalabanov_e53b30b6/localhelp-a-map-where-neighbors-help-each-other-in-one-click-dev-weekend-challenge-community-9in</link>
      <guid>https://dev.to/roman_shalabanov_e53b30b6/localhelp-a-map-where-neighbors-help-each-other-in-one-click-dev-weekend-challenge-community-9in</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;I'm from Ukraine. Over here, volunteering and mutual aid are not buzzwords. They are survival skills.&lt;/p&gt;

&lt;p&gt;Since 2022, our communities have learned to self-organize at a speed that surprises even ourselves. When pharmacies run out of ibuprofen, a neighbor finds it. When an elderly person needs a ride to the hospital, someone nearby drives them. When the power is out and you need a phone charger, you post in a group chat and three people respond in minutes.&lt;/p&gt;

&lt;p&gt;This happens every day. But it happens through messy group chats, lost messages, and zero structure. Requests get buried. People who want to help never see the ones who need it.&lt;/p&gt;

&lt;p&gt;LocalHelp is my attempt to fix that: &lt;strong&gt;a map where you post what you need, and the closest person who can help sees it first.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LocalHelp&lt;/strong&gt; is a real-time, map-based micro-volunteering platform. The idea is dead simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You see a map of your neighborhood&lt;/li&gt;
&lt;li&gt;You pin a request for help (groceries, medicine, transport, anything)&lt;/li&gt;
&lt;li&gt;A neighbor sees it, clicks "I'll help", and both of you instantly get each other's contact&lt;/li&gt;
&lt;li&gt;When the help arrives, you close the request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No middleman. No coordination overhead. No app store. Just a browser and a map.&lt;/p&gt;

&lt;p&gt;The whole interaction takes about 15 seconds from "I need help" to "someone is on the way."&lt;/p&gt;

&lt;h3&gt;
  
  
  The map tells you everything at a glance
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fds8wj0jk49m0yg5nsiyi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fds8wj0jk49m0yg5nsiyi.png" alt="Map overview with colored markers and popup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every category has its own color: blue for groceries, red for medicine, purple for transport, teal for everything else. You see the whole picture without clicking a single marker. Filter by category, draw a custom area on the map, and the list updates in real time.&lt;/p&gt;

&lt;p&gt;Tap any marker to see the full details, contact info, and a single "I'll help" button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Once a neighbor takes your request, you know immediately
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl35umc561le39f47axn9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl35umc561le39f47axn9.png" alt="Owner view with helper info and visual markers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No page reload. No email notification to check later. The marker turns orange the moment someone takes it, and the helper's name and contact appear right in the popup. WebSockets make this instant for everyone on the map.&lt;/p&gt;

&lt;p&gt;Your own requests have a white center dot so you can always spot them. The "Mark done" button closes the loop when help has arrived.&lt;/p&gt;

&lt;h3&gt;
  
  
  Click any marker for full details
&lt;/h3&gt;

&lt;p&gt;Tap a marker to open a popup with everything you need: description, category, contact info, and deadline. If it is open, you will see a big "I'll help" button. If someone already took it, you will see who is helping.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8huedspmxdwtvzrd4nyb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8huedspmxdwtvzrd4nyb.png" alt="Marker popup — open request with I'll help button"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth80sbs5yrogwiw0o4ir.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth80sbs5yrogwiw0o4ir.png" alt="Marker popup — taken request with helper info"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Track everything you asked for in one place
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flggqygn8ccus44mfsjy9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flggqygn8ccus44mfsjy9.png" alt="My Needs modal showing in-progress and fulfilled requests"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "My Needs" panel shows all your active requests: who took them, their contact info, deadline, and full history. Fulfilled requests stay visible until they expire, so nothing disappears when you are not looking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manage what you are helping with
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk0244w77nhe7bc3pjln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk0244w77nhe7bc3pjln.png" alt="My Help modal showing assigned tasks with deadlines"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "My Help" panel is your volunteer dashboard. Every task you committed to, with the requester's contact, category, and hard deadline. Changed your mind? Hit "Give up" and the request goes back to the map as open. No guilt, no friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/1Y72TaLeX1Y"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Here is how to run it yourself in under 2 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/romansh/localhelp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;localhelp
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php artisan key:generate
&lt;span class="nb"&gt;touch &lt;/span&gt;database/database.sqlite &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php artisan migrate &lt;span class="nt"&gt;--seed&lt;/span&gt;
npm run build
php artisan serve          &lt;span class="c"&gt;# App&lt;/span&gt;
php artisan reverb:start   &lt;span class="c"&gt;# WebSocket server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Docker with Traefik + Cloudflare Tunnel is also supported:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/romansh" rel="noopener noreferrer"&gt;
        romansh
      &lt;/a&gt; / &lt;a href="https://github.com/romansh/localhelp" rel="noopener noreferrer"&gt;
        localhelp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      LocalHelp: find or offer help in your neighborhood with real-time map notifications.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LocalHelp&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Neighbors helping neighbors — one click on the map.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LocalHelp is a real-time, map-based micro-volunteering platform. Anyone can pin a request for help on the map — groceries, medicine, transport, anything. A neighbor sees it, clicks "I'll help", and both sides instantly get each other's contact. No middleman, no coordination overhead.&lt;/p&gt;

&lt;p&gt;Built for the &lt;strong&gt;DEV Weekend Challenge&lt;/strong&gt; in 48 hours.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How It Works&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;1. Browse requests near you&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;Color-coded markers make the whole city readable at a glance — blue for food, red for medicine, purple for transport, teal for other. Filter by category, draw a custom area on the map, and only see what's relevant to you.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/romansh/localhelp/docs/images/Localhelp-1.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fromansh%2Flocalhelp%2Fdocs%2Fimages%2FLocalhelp-1.png" alt="Map overview with popup"&gt;&lt;/a&gt;
&lt;em&gt;Tap any marker to see the full request, contact details, and a single "I'll help" button&lt;/em&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;2. Take a request — your neighbors see it instantly&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;When you click "I'll help", the marker gets an &lt;strong&gt;orange ring&lt;/strong&gt; and a status dot…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/romansh/localhelp" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;Key parts of the codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Livewire v4 components&lt;/strong&gt; handle all interactivity without writing a single REST endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Reverb&lt;/strong&gt; (WebSockets) pushes marker updates to every connected client in real time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaflet.js + Leaflet Draw&lt;/strong&gt; power the map, custom markers, area selection, and popups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google OAuth&lt;/strong&gt; for one-click login (no passwords to manage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-spam&lt;/strong&gt;: reCAPTCHA, daily rate limits, keyword blacklist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-expiration&lt;/strong&gt;: requests disappear after 1h to 7 days (user's choice)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt;: zero infrastructure, works out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Laravel 12, Livewire v4, Alpine.js, TailwindCSS v4, Leaflet.js, Laravel Reverb, SQLite&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Friday + Saturday&lt;/strong&gt; - thinking, sketching, choosing between ideas. My first concept was actually a tournament scheduling app for a local table tennis league I play in. It would account for power outages and air raid alerts when rescheduling matches. That is a real problem where I live. It would have been a fun build, but the audience is tiny and the potential stops at one league. Then I thought: what if I build something that any neighborhood can use, and that can later be narrowed down to any niche? That is how LocalHelp was born.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunday&lt;/strong&gt; - all the coding. Database schema, Google OAuth, Leaflet map, markers, popups, CRUD, real-time broadcasting, category filters, area selection, helper workflow, My Needs / My Help panels, visual marker system, Ukrainian translation, spam protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunday night into Monday&lt;/strong&gt; - Docker setup, polish, screenshots, this post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Design decisions that mattered:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map-first, not list-first.&lt;/strong&gt; When you need help from a neighbor, distance is the most important filter. A list cannot show you that. A map does it instantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No chat, no comments, no threads.&lt;/strong&gt; The app gives you a phone number or Telegram handle. You call. You text. The real conversation happens where people already communicate. The app just connects you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual status on markers.&lt;/strong&gt; You should never have to click a marker to know if a request is taken or fulfilled. Orange ring = someone is helping. Faded = done. White dot = yours. This was inspired by how traffic lights work: color carries meaning before you read anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQLite by default.&lt;/strong&gt; This is a neighborhood tool. It does not need Postgres until it serves 10,000 people. Zero-config setup means anyone can fork it and run it for their block in under 2 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why this matters beyond a hackathon:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LocalHelp is intentionally generic. It could be the starting point for something more specific: a tool for a running club to coordinate rides to races, a neighborhood watch reporting system, a mutual aid network for a refugee community, or a disaster response coordination board. The map-first, real-time, one-click-to-connect pattern works for any community where proximity matters. Fork it, change the categories, and you have a boilerplate for your own niche.&lt;/p&gt;

&lt;p&gt;In Ukraine, we learned that the best systems are the ones people actually use under stress. They need to be fast, obvious, and forgiving. That is what I tried to build this weekend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on AI:&lt;/strong&gt; I used GitHub Copilot Chat in VS Code. Maybe a little. Maybe not so little. But here is the thing: Copilot does not know what your community needs. It does not know that orange means "taken" and gray means "done" in your head. It does not know that the popup should show a phone number, not an email. AI is a power tool, but you still need to know what you are building and why. The intent, the UX decisions, the architecture, the flow from "I need help" to "someone is coming" - that is all human.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with care from Ukraine.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Laravel SDK for Creem.io: multi-profile billing, webhook events, and an interactive demo</title>
      <dc:creator>Roman Shalabanov</dc:creator>
      <pubDate>Fri, 27 Feb 2026 11:55:47 +0000</pubDate>
      <link>https://dev.to/roman_shalabanov_e53b30b6/building-a-laravel-sdk-for-creemio-multi-profile-billing-webhook-events-and-an-interactive-demo-30ed</link>
      <guid>https://dev.to/roman_shalabanov_e53b30b6/building-a-laravel-sdk-for-creemio-multi-profile-billing-webhook-events-and-an-interactive-demo-30ed</guid>
      <description>&lt;p&gt;I recently open-sourced a Laravel SDK for &lt;a href="https://creem.io" rel="noopener noreferrer"&gt;Creem.io&lt;/a&gt; and wanted to write up the story behind it, because the path to building it was a bit roundabout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;My existing project uses &lt;a href="https://github.com/thephpleague/omnipay" rel="noopener noreferrer"&gt;Omnipay&lt;/a&gt;, the PHP League's payment abstraction library (not a payment provider itself), to handle checkout through multiple gateways via a single interface. I originally planned to stick with a provider that already had an Omnipay driver. But mid-integration I switched to Creem. Since the project was already wired through Omnipay, I wrote a driver for it: &lt;strong&gt;&lt;a href="https://github.com/romansh/omnipay-creem" rel="noopener noreferrer"&gt;romansh/omnipay-creem&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Omnipay is a solid choice when you need to swap gateways with one line of code. The trade-off is that it's a lowest-common-denominator abstraction: you get &lt;code&gt;purchase()&lt;/code&gt; and &lt;code&gt;completePurchase()&lt;/code&gt;, and everything else (webhook routing, event dispatching, config management, retry logic) you have to build yourself.&lt;/p&gt;

&lt;p&gt;At some point I discovered Creem had a developer bounty for an official Laravel SDK. Since I was already working with their API and had a feel for what was missing, I decided to build it properly: a Laravel-native package that handles all that boilerplate out of the box. If a package like this had existed when I started, I probably would have used it instead of writing the Omnipay driver.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this article is also part of that bounty. That said, the packages fill a real gap and I would have written this up regardless.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem with typical payment integrations
&lt;/h2&gt;

&lt;p&gt;Most payment integrations are built around one API key per app. That works until you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy&lt;/strong&gt;: each tenant with their own billing account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple storefronts&lt;/strong&gt;: different products or brands on separate Creem accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging vs production&lt;/strong&gt;: without touching &lt;code&gt;.env&lt;/code&gt; per environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Departmental billing&lt;/strong&gt;: isolated billing within the same app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/romansh/laravel-creem" rel="noopener noreferrer"&gt;romansh/laravel-creem&lt;/a&gt;&lt;/strong&gt; is a full-featured SDK with Laravel-native patterns.&lt;/p&gt;

&lt;p&gt;What's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete API coverage&lt;/strong&gt;: Products, Checkouts, Customers, Subscriptions, Transactions, Licenses, Discount Codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-profile config&lt;/strong&gt;: switch API keys per request with &lt;code&gt;Creem::profile('store_b')&lt;/code&gt; or override inline with &lt;code&gt;Creem::withConfig([...])&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks done right&lt;/strong&gt;: auto-registered routes, HMAC signature verification, typed Laravel events including &lt;code&gt;GrantAccess&lt;/code&gt; / &lt;code&gt;RevokeAccess&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artisan tooling&lt;/strong&gt;: &lt;code&gt;php artisan creem:test-webhook checkout.completed&lt;/code&gt; for local testing without hitting the real API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laravel 10 / 11 / 12, PHP 8.1-8.4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well tested&lt;/strong&gt;: unit + feature tests, PSR-12, full PHPDoc&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create a checkout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Romansh\LaravelCreem\Facades\Creem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Creem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;checkouts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'success_url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'customer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'checkout_url'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-profile in action
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use a different Creem account per product line&lt;/span&gt;
&lt;span class="nv"&gt;$txns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Creem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product_a'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Webhook listener: grant access on payment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GrantAccess&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GrantAccess&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'plan'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'subscribed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quick start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require romansh/laravel-creem
php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;creem-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;CREEM_API_KEY&lt;/code&gt; and &lt;code&gt;CREEM_WEBHOOK_SECRET&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and you're good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive demo app
&lt;/h2&gt;

&lt;p&gt;There's also &lt;strong&gt;&lt;a href="https://github.com/romansh/laravel-creem-demo" rel="noopener noreferrer"&gt;romansh/laravel-creem-demo&lt;/a&gt;&lt;/strong&gt;, a full working app (Laravel 12 + Livewire + Tailwind) built to explore every feature through a web UI: configure API keys in the browser, create products, trigger checkouts, manage subscriptions, and watch webhook events arrive in real time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer create-project romansh/laravel-creem-demo my-creem-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-creem-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker setup includes an optional Cloudflare Tunnel so webhooks work locally without any port forwarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;API Setup: credentials, webhook URL, profile tabs:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxefzoeu24nijwu7dj6fq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxefzoeu24nijwu7dj6fq.png" alt="API Setup: credentials, webhook URL, profile tabs" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Webhooks and Access: live event log, GrantAccess/RevokeAccess in real time:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4o1woo2dwi34kdmr2b4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4o1woo2dwi34kdmr2b4h.png" alt="Webhooks and Access: live event log, GrantAccess/RevokeAccess in real time" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Transactions: payment history with amounts and customer emails:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytfmugnyhusw17j5vp7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytfmugnyhusw17j5vp7d.png" alt="Transactions: payment history with amounts and customer emails" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Discounts: create percentage/fixed discount codes:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gzekm6vzr6kc6cul760.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gzekm6vzr6kc6cul760.png" alt="Discounts: create percentage/fixed discount codes" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Subscriptions: recurring plans with billing periods:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6aqe5fefcfzo9j4e46dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6aqe5fefcfzo9j4e46dq.png" alt="Subscriptions: recurring plans with billing periods" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One-Time Payments: product preview modal, checkout flow:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrk3modysi85xsq09b4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrk3modysi85xsq09b4d.png" alt="One-Time Payments: product preview modal, checkout flow" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel SDK:&lt;/strong&gt; &lt;a href="https://github.com/romansh/laravel-creem" rel="noopener noreferrer"&gt;github.com/romansh/laravel-creem&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo app:&lt;/strong&gt; &lt;a href="https://github.com/romansh/laravel-creem-demo" rel="noopener noreferrer"&gt;github.com/romansh/laravel-creem-demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Omnipay driver:&lt;/strong&gt; &lt;a href="https://github.com/romansh/omnipay-creem" rel="noopener noreferrer"&gt;github.com/romansh/omnipay-creem&lt;/a&gt; (framework-agnostic, for projects already using Omnipay)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and issues are welcome on GitHub or in the comments below. If you end up using one of these packages, I'd be happy to hear how it works out.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Laravel SDK for Creem.io: multi-profile billing, webhook events, and an interactive demo</title>
      <dc:creator>Roman Shalabanov</dc:creator>
      <pubDate>Fri, 27 Feb 2026 11:55:47 +0000</pubDate>
      <link>https://dev.to/roman_shalabanov_e53b30b6/building-a-laravel-sdk-for-creemio-multi-profile-billing-webhook-events-and-an-interactive-demo-25hg</link>
      <guid>https://dev.to/roman_shalabanov_e53b30b6/building-a-laravel-sdk-for-creemio-multi-profile-billing-webhook-events-and-an-interactive-demo-25hg</guid>
      <description>&lt;p&gt;I recently open-sourced a Laravel SDK for &lt;a href="https://creem.io" rel="noopener noreferrer"&gt;Creem.io&lt;/a&gt; and wanted to write up the story behind it, because the path to building it was a bit roundabout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;My existing project uses &lt;a href="https://github.com/thephpleague/omnipay" rel="noopener noreferrer"&gt;Omnipay&lt;/a&gt;, the PHP League's payment abstraction library (not a payment provider itself), to handle checkout through multiple gateways via a single interface. I originally planned to stick with a provider that already had an Omnipay driver. But mid-integration I switched to Creem. Since the project was already wired through Omnipay, I wrote a driver for it: &lt;strong&gt;&lt;a href="https://github.com/romansh/omnipay-creem" rel="noopener noreferrer"&gt;romansh/omnipay-creem&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Omnipay is a solid choice when you need to swap gateways with one line of code. The trade-off is that it's a lowest-common-denominator abstraction: you get &lt;code&gt;purchase()&lt;/code&gt; and &lt;code&gt;completePurchase()&lt;/code&gt;, and everything else (webhook routing, event dispatching, config management, retry logic) you have to build yourself.&lt;/p&gt;

&lt;p&gt;At some point I discovered Creem had a developer bounty for an official Laravel SDK. Since I was already working with their API and had a feel for what was missing, I decided to build it properly: a Laravel-native package that handles all that boilerplate out of the box. If a package like this had existed when I started, I probably would have used it instead of writing the Omnipay driver.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this article is also part of that bounty. That said, the packages fill a real gap and I would have written this up regardless.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem with typical payment integrations
&lt;/h2&gt;

&lt;p&gt;Most payment integrations are built around one API key per app. That works until you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy&lt;/strong&gt;: each tenant with their own billing account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple storefronts&lt;/strong&gt;: different products or brands on separate Creem accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging vs production&lt;/strong&gt;: without touching &lt;code&gt;.env&lt;/code&gt; per environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Departmental billing&lt;/strong&gt;: isolated billing within the same app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/romansh/laravel-creem" rel="noopener noreferrer"&gt;romansh/laravel-creem&lt;/a&gt;&lt;/strong&gt; is a full-featured SDK with Laravel-native patterns.&lt;/p&gt;

&lt;p&gt;What's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete API coverage&lt;/strong&gt;: Products, Checkouts, Customers, Subscriptions, Transactions, Licenses, Discount Codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-profile config&lt;/strong&gt;: switch API keys per request with &lt;code&gt;Creem::profile('store_b')&lt;/code&gt; or override inline with &lt;code&gt;Creem::withConfig([...])&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks done right&lt;/strong&gt;: auto-registered routes, HMAC signature verification, typed Laravel events including &lt;code&gt;GrantAccess&lt;/code&gt; / &lt;code&gt;RevokeAccess&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artisan tooling&lt;/strong&gt;: &lt;code&gt;php artisan creem:test-webhook checkout.completed&lt;/code&gt; for local testing without hitting the real API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laravel 10 / 11 / 12, PHP 8.1-8.4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well tested&lt;/strong&gt;: unit + feature tests, PSR-12, full PHPDoc&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create a checkout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Romansh\LaravelCreem\Facades\Creem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Creem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;checkouts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'success_url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'customer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'checkout_url'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-profile in action
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use a different Creem account per product line&lt;/span&gt;
&lt;span class="nv"&gt;$txns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Creem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product_a'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Webhook listener: grant access on payment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GrantAccess&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GrantAccess&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'plan'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'subscribed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quick start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require romansh/laravel-creem
php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;creem-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;CREEM_API_KEY&lt;/code&gt; and &lt;code&gt;CREEM_WEBHOOK_SECRET&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and you're good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive demo app
&lt;/h2&gt;

&lt;p&gt;There's also &lt;strong&gt;&lt;a href="https://github.com/romansh/laravel-creem-demo" rel="noopener noreferrer"&gt;romansh/laravel-creem-demo&lt;/a&gt;&lt;/strong&gt;, a full working app (Laravel 12 + Livewire + Tailwind) built to explore every feature through a web UI: configure API keys in the browser, create products, trigger checkouts, manage subscriptions, and watch webhook events arrive in real time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer create-project romansh/laravel-creem-demo my-creem-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-creem-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker setup includes an optional Cloudflare Tunnel so webhooks work locally without any port forwarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxefzoeu24nijwu7dj6fq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxefzoeu24nijwu7dj6fq.png" alt="API Setup: credentials, webhook URL, profile tabs" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;API Setup: credentials, webhook URL, profile tabs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrk3modysi85xsq09b4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrk3modysi85xsq09b4d.png" alt="Webhooks and Access: live event log, GrantAccess/RevokeAccess in real time" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Webhooks and Access: live event log, GrantAccess/RevokeAccess in real time&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6aqe5fefcfzo9j4e46dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6aqe5fefcfzo9j4e46dq.png" alt="Transactions: payment history with amounts and customer emails" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Transactions: payment history with amounts and customer emails&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gzekm6vzr6kc6cul760.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gzekm6vzr6kc6cul760.png" alt="Discounts: create percentage/fixed discount codes" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Discounts: create percentage/fixed discount codes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytfmugnyhusw17j5vp7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytfmugnyhusw17j5vp7d.png" alt="Subscriptions: recurring plans with billing periods" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Subscriptions: recurring plans with billing periods&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4o1woo2dwi34kdmr2b4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4o1woo2dwi34kdmr2b4h.png" alt="One-Time Payments: product preview modal, checkout flow" width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One-Time Payments: product preview modal, checkout flow&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel SDK:&lt;/strong&gt; &lt;a href="https://github.com/romansh/laravel-creem" rel="noopener noreferrer"&gt;github.com/romansh/laravel-creem&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo app:&lt;/strong&gt; &lt;a href="https://github.com/romansh/laravel-creem-demo" rel="noopener noreferrer"&gt;github.com/romansh/laravel-creem-demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Omnipay driver:&lt;/strong&gt; &lt;a href="https://github.com/romansh/omnipay-creem" rel="noopener noreferrer"&gt;github.com/romansh/omnipay-creem&lt;/a&gt; (framework-agnostic, for projects already using Omnipay)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and issues are welcome on GitHub or in the comments below. If you end up using one of these packages, I'd be happy to hear how it works out.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
