<?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: Rebel Studios</title>
    <description>The latest articles on DEV Community by Rebel Studios (@rebel_studios).</description>
    <link>https://dev.to/rebel_studios</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4094544%2F467b9d90-2397-444d-976f-4d094830cfdd.png</url>
      <title>DEV Community: Rebel Studios</title>
      <link>https://dev.to/rebel_studios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rebel_studios"/>
    <language>en</language>
    <item>
      <title>Reading Zora Creator Rewards on Base When Nothing Is Indexed</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:15:30 +0000</pubDate>
      <link>https://dev.to/rebel_studios/reading-zora-creator-rewards-on-base-when-nothing-is-indexed-4fk0</link>
      <guid>https://dev.to/rebel_studios/reading-zora-creator-rewards-on-base-when-nothing-is-indexed-4fk0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://rebelstudiossoftware.com/blog-zora-creator-rewards-onchain.html" rel="noopener noreferrer"&gt;rebelstudiossoftware.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every trade of a Zora coin pays several people at once: the coin’s creator, the platform that launched it, the app that routed the trade, the protocol, and Doppler. If you create coins, run a platform that launches them, or build an app that passes a referrer on trades, some of that money is yours. Zora’s REST API won’t tell you how much. The payouts are onchain, one event per trade, and getting yours out takes one idea and some patience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The event, and the problem with it
&lt;/h2&gt;

&lt;p&gt;Coins launched since Zora moved to Uniswap V4 pay rewards through their hook, which emits &lt;code&gt;CoinMarketRewardsV4&lt;/code&gt;. Its payload is seven addresses (the coin, the currency the reward is paid in, then the creator, platform referrer, trade referrer, protocol and Doppler recipients) followed by ten amounts: for each of those five recipients, what they got in the currency and what they got in the coin itself.&lt;/p&gt;

&lt;p&gt;None of those fields is declared &lt;code&gt;indexed&lt;/code&gt;, and that one detail decides everything. A log’s indexed fields go into its topics, and topics are what &lt;code&gt;eth_getLogs&lt;/code&gt; filters on. This event has a single topic, its own signature. So you can ask a node for every &lt;code&gt;CoinMarketRewardsV4&lt;/code&gt; event in a block range, but not for the ones that pay you. Your address is in the data, where the node doesn’t look.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39p1bkivmkk6qd5uf51u.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39p1bkivmkk6qd5uf51u.png" alt="Diagram of one CoinMarketRewardsV4 log: the only topic is the event signature, and the creator, platform referrer and trade referrer addresses sit in data words 2 to 4" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coins from before the V4 migration use an older event, &lt;code&gt;CoinTradeRewards&lt;/code&gt;, which does index its three recipients, so an ordinary address filter works for them. For anything launched since mid-2025, it doesn’t.&lt;/p&gt;

&lt;p&gt;One more V4 event pays creators. On creator-coin trades the hook also emits &lt;code&gt;CreatorCoinRewards&lt;/code&gt;: the coin (indexed), the currency, the creator and the protocol, then the creator’s and the protocol’s amounts. The recipients aren’t indexed there either, and the event is easy to miss, but it matters: in a sample day on Base it carried about a third of all payouts and nearly half of what creators earned. A scan that reads only &lt;code&gt;CoinMarketRewardsV4&lt;/code&gt; under-counts a creator by that much.&lt;/p&gt;

&lt;h2&gt;
  
  
  So read all of them
&lt;/h2&gt;

&lt;p&gt;The fix is unglamorous: fetch every reward event in a range, decode it, and keep the ones where your address is one of the three recipients. On Base today that’s about 400 events per 2,000 blocks. Here’s the whole thing with nothing but JSON-RPC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import httpx

RPC = "https://mainnet.base.org"
# keccak256 of the two V4 payout events' signatures (topic 0 of each log)
MARKET = "0x35b5031218696db1dfd903223a47f38e66a1998e14a942a5d60fddaa49a685fc"        # CoinMarketRewardsV4
CREATOR_COIN = "0xea92473287be4e55f8279d0b8395a45960a217ae2f1a76ac9cae84af58a751ed"  # CreatorCoinRewards
ME = "0x55c88bb05602da94fce8feadc1cbebf5b72c2453"

def rpc(method, params):
    body = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
    return httpx.post(RPC, json=body, timeout=30).json()["result"]

head = int(rpc("eth_blockNumber", []), 16)
logs = rpc("eth_getLogs", [{"fromBlock": hex(head - 2000), "toBlock": hex(head),
                            "topics": [[MARKET, CREATOR_COIN]]}])

addr = lambda word: "0x" + word[-20:].hex()
for log in logs:
    data = bytes.fromhex(log["data"][2:])
    w = [data[i:i + 32] for i in range(0, len(data), 32)]
    if log["topics"][0] == MARKET:
        # words 0-6: addresses; 7-16: amounts (per role, in currency then in the coin)
        currency = w[1]
        roles = [("creator", w[2], w[7]), ("platform referrer", w[3], w[9]),
                 ("trade referrer", w[4], w[11])]
    else:
        # the coin is in topic 1; words: currency, creator, protocol, then their two amounts
        currency = w[0]
        roles = [("creator", w[1], w[3])]
    for role, who, amount in roles:
        if addr(who) == ME:
            block = int(log["blockNumber"], 16)
            print(block, role, int.from_bytes(amount, "big"), "of", addr(currency))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data is plain ABI encoding: 32-byte words with addresses right-aligned and amounts as big-endian integers. &lt;code&gt;CoinMarketRewardsV4&lt;/code&gt; has seven addresses then ten amounts; &lt;code&gt;CreatorCoinRewards&lt;/code&gt; has three addresses then two. Amounts are in the token’s smallest unit, so &lt;code&gt;486910&lt;/code&gt; of USDC (&lt;code&gt;0x8335…2913&lt;/code&gt;) is 0.486910 USDC, and 18-decimal tokens like ZORA and WETH need dividing by 1018.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scanning a real window
&lt;/h2&gt;

&lt;p&gt;One call covers 2,000 blocks. Base makes a block every two seconds, so that’s about 67 minutes; a day is 43,200 blocks (22 calls) and 30 days is about 650 calls. A few details make that practical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time is arithmetic.&lt;/strong&gt; Base blocks are exactly two seconds apart, so a block’s timestamp is &lt;code&gt;1686789347 + 2 × block&lt;/code&gt;. You don’t need an &lt;code&gt;eth_getBlockByNumber&lt;/code&gt; per event to date it, and “the last seven days” is a subtraction from the head block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start where the events start.&lt;/strong&gt; The first V4 reward events show up around block 31,000,000, in June 2025. There’s nothing to find before that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect range errors.&lt;/strong&gt; Public RPCs cap the block range or the number of results per call. When one refuses, halve the range and retry instead of failing the whole scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember what you’ve scanned.&lt;/strong&gt; Store the block ranges you’ve covered for each address and only fetch what’s new next time. The second run of a 30-day report should take seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price it at the end.&lt;/strong&gt; Rewards arrive in ZORA, WETH, USDC or a creator coin. Sum raw amounts per role and token first, and convert to dollars once, when you report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Or use the library
&lt;/h2&gt;

&lt;p&gt;All of that is the rewards module in the &lt;a href="https://rebelstudiossoftware.com/zora-coins-sdk.html" rel="noopener noreferrer"&gt;Zora Coins SDKs&lt;/a&gt;, which are open source and MIT licensed. In Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from zora_coins.rewards import RewardsIndexer, build_report, to_text

me = "0x55c88bb05602da94fce8feadc1cbebf5b72c2453"
with RewardsIndexer("rewards.sqlite") as idx:
    idx.scan([me], days=7)          # re-running only fetches blocks it hasn't seen
    print(to_text(build_report(idx.events_for([me]), [me])))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For about an hour and a half of blocks, that printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zora rewards for 0x55c88bb05602da94fce8feadc1cbebf5b72c2453
114 reward events, blocks 51477712–51479837
  Platform referral          145.7255 ZORA                $1.18  (7 payouts)
  Platform referral            2.1861 USDC                $2.19  (25 payouts)
  Trade referral               6.7341 ZORA                $0.05  (69 payouts)
  Trade referral          6.21437e-05 WETH                $0.16  (14 payouts)
  Trade referral             0.175478 USDC                $0.18  (14 payouts)
  Total (current prices)                                      $3.76
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same indexer is in the TypeScript, Go, Rust, C#, Java and C++ SDKs, and the Python, TypeScript, Go and Rust packages include it as a &lt;code&gt;zora-rewards&lt;/code&gt; command (&lt;code&gt;pip install zora-coins&lt;/code&gt;, then &lt;code&gt;zora-rewards --days 7 0xYourAddress&lt;/code&gt;). All seven give the same totals, to the wei, on the same set of recorded events. That cross-check is how I know the word offsets above are right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Unofficial
&lt;/h3&gt;

&lt;p&gt;The SDKs are community-maintained by Rebel Studios and not affiliated with Zora. If Zora changes the event, the recorded-event tests will be the first thing to fail, and the fix will go out as a new version in every language at once.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>web3</category>
      <category>python</category>
      <category>javascript</category>
    </item>
    <item>
      <title>ATS-Friendly Resume Format: What Actually Parses</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Thu, 17 Sep 2026 11:21:57 +0000</pubDate>
      <link>https://dev.to/rebel_studios/ats-friendly-resume-format-what-actually-parses-1af8</link>
      <guid>https://dev.to/rebel_studios/ats-friendly-resume-format-what-actually-parses-1af8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://rebelstudiossoftware.com/blog-ats-friendly-resume-format.html" rel="noopener noreferrer"&gt;rebelstudiossoftware.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most “my resume gets no responses” problems are not about your experience — they’re about &lt;strong&gt;format&lt;/strong&gt;. An Applicant Tracking System has to parse your document into fields, and the wrong layout garbles it before a recruiter ever searches. Here is the format that actually parses cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  File type: .docx beats a design-tool PDF
&lt;/h2&gt;

&lt;p&gt;A Word &lt;code&gt;.docx&lt;/code&gt; is the safest, most universally parseable format. PDFs &lt;em&gt;can&lt;/em&gt; parse well — but only if the text is real, selectable text. A PDF exported from a design tool (Canva, InDesign) where the resume is effectively an image will parse as nothing. If you must send PDF, make sure you can select and copy the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  One column. Always.
&lt;/h2&gt;

&lt;p&gt;Multi-column layouts, tables, and text boxes are the number-one parsing killer. The parser reads left-to-right, top-to-bottom; a two-column design interleaves your sidebar into your job history and turns it to nonsense. Use a single column, top to bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headings, fonts, and the small stuff
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standard section headings:&lt;/strong&gt; “Work Experience,” “Education,” “Skills.” Creative headings (“Where I’ve Made Impact”) can miss the parser’s expected buckets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Common fonts:&lt;/strong&gt; Arial, Calibri, Georgia, Times. Nothing exotic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No headers/footers&lt;/strong&gt; for contact info — many parsers ignore that region. Put your name, email and phone in the body.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No graphics, logos, icons or photos.&lt;/strong&gt; They carry no parseable text and can break layout.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consistent dates:&lt;/strong&gt; one format, e.g. &lt;code&gt;Jan 2023 – Mar 2025&lt;/code&gt;, everywhere.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test it before you trust it
&lt;/h2&gt;

&lt;p&gt;The fastest sanity check: copy all the text out of your resume and paste it into a plain notepad. If the order is scrambled or fields are missing, the ATS sees that same mess. Then check the actual content against each posting — the right format gets you parsed; the right &lt;a href="https://rebelstudiossoftware.com/blog-ats-resume-keywords.html" rel="noopener noreferrer"&gt;keywords&lt;/a&gt; get you ranked.&lt;/p&gt;

&lt;h3&gt;
  
  
  See your match before you apply
&lt;/h3&gt;

&lt;p&gt;Resume Keyword Matcher is a free Chrome and Firefox extension that reads any job posting, pulls its keywords, and scores how well your resume matches — showing you exactly which terms you’re missing so you can close the gaps before an ATS ever ranks you. Everything runs on your device; your resume never leaves your computer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rebelstudiossoftware.com/resume-matcher.html?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=resume-matcher" rel="noopener noreferrer"&gt;Get the free extension →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related reading: &lt;a href="https://rebelstudiossoftware.com/blog-get-resume-past-ats.html" rel="noopener noreferrer"&gt;how to get your resume past an ATS&lt;/a&gt; and &lt;a href="https://rebelstudiossoftware.com/blog-ats-resume-keywords.html" rel="noopener noreferrer"&gt;ATS resume keywords&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>career</category>
      <category>jobsearch</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Selling event tickets on WooCommerce without the per-ticket tax</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Tue, 15 Sep 2026 11:19:28 +0000</pubDate>
      <link>https://dev.to/rebel_studios/selling-event-tickets-on-woocommerce-without-the-per-ticket-tax-33ek</link>
      <guid>https://dev.to/rebel_studios/selling-event-tickets-on-woocommerce-without-the-per-ticket-tax-33ek</guid>
      <description>&lt;p&gt;If you run events and a WordPress site, the ticketing math is quietly&lt;br&gt;
brutal. Eventbrite takes roughly &lt;strong&gt;$1.79 + 3.7% per ticket&lt;/strong&gt;. Sell 300 tickets at&lt;br&gt;
$40 and you've handed over ~$500 before your own payment processor takes its cut.&lt;br&gt;
And the "free" WordPress ticketing plugins mostly aren't — the common pattern is a&lt;br&gt;
free RSVP tier with the &lt;em&gt;actual paid-ticket feature&lt;/em&gt; locked behind a $75+ upgrade,&lt;br&gt;
sometimes just to turn on Stripe.&lt;/p&gt;

&lt;p&gt;Here's the thing: if you already run WooCommerce, you don't need any of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WooCommerce-native ticketing avoids the fees
&lt;/h2&gt;

&lt;p&gt;WooCommerce already has your payment gateway configured — Stripe, PayPal, whatever&lt;br&gt;
you use. It already handles tax, currency, receipts, and refunds. A ticket is just a&lt;br&gt;
product. So "event ticketing" doesn't need to be a separate platform with its own&lt;br&gt;
cut; it can be a thin layer on top of the checkout you already run.&lt;/p&gt;

&lt;p&gt;That changes the fee structure completely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No per-ticket platform fee.&lt;/strong&gt; You pay your normal payment-processor rate (~2.9%

&lt;ul&gt;
&lt;li&gt;30¢) and nothing else. On 300 tickets at $40 that's the difference between
keeping ~$11,600 vs ~$11,100 — and none of it routed through a third party.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The money lands in your own account&lt;/strong&gt;, same as every other WooCommerce order —
not held by a ticketing company on their payout schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No forced paywall on payments.&lt;/strong&gt; Taking money should not itself be the premium
feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "good enough" ticketing actually needs
&lt;/h2&gt;

&lt;p&gt;For the vast majority of organizers — a workshop, a supper club, a meetup, a class&lt;br&gt;
series — the real requirements are small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put a price on an event and take payment through your existing checkout.&lt;/li&gt;
&lt;li&gt;Issue a &lt;strong&gt;QR-coded ticket by email&lt;/strong&gt;, automatically, once the order is paid (not
when someone abandons checkout — that should never issue a ticket or eat a seat).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan the QR at the door&lt;/strong&gt; to check people in — no separate hardware, no app.&lt;/li&gt;
&lt;li&gt;Optionally, &lt;strong&gt;multiple ticket types&lt;/strong&gt; — General / VIP / Early bird — each with its
own price and its own capacity that sells out independently.&lt;/li&gt;
&lt;li&gt;An attendee list and CSV export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. Seat maps and reserved seating are a genuinely bigger problem, but most&lt;br&gt;
organizers don't need them and shouldn't pay for a platform that assumes they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build/buy version
&lt;/h2&gt;

&lt;p&gt;I build small WordPress tools, and I made exactly this so I'd stop recommending&lt;br&gt;
Eventbrite to people who already had WooCommerce: &lt;strong&gt;Event Tickets &amp;amp; Registration&lt;/strong&gt;.&lt;br&gt;
The free plugin does unlimited RSVP events with QR tickets and door check-in; the&lt;br&gt;
&lt;a href="https://rebelstudios.gumroad.com/l/cowxt" rel="noopener noreferrer"&gt;Pro add-on&lt;/a&gt; adds paid tickets through your&lt;br&gt;
own WooCommerce checkout and multiple ticket types — one-time price, &lt;strong&gt;no per-ticket&lt;br&gt;
cut, ever&lt;/strong&gt;. It's a separate download and not required; the free tier stands on its&lt;br&gt;
own.&lt;/p&gt;

&lt;p&gt;But the real point stands whether you use it or not: &lt;strong&gt;if you're on WooCommerce,&lt;br&gt;
you're already paying for the infrastructure ticketing platforms resell back to you.&lt;br&gt;
Stop paying the per-ticket tax on top.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>ecommerce</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How to structure a freemium WordPress plugin that passes wp.org review</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Tue, 15 Sep 2026 01:51:33 +0000</pubDate>
      <link>https://dev.to/rebel_studios/how-to-structure-a-freemium-wordpress-plugin-that-passes-wporg-review-2hlf</link>
      <guid>https://dev.to/rebel_studios/how-to-structure-a-freemium-wordpress-plugin-that-passes-wporg-review-2hlf</guid>
      <description>&lt;p&gt;If WordPress.org rejected your plugin for &lt;strong&gt;"trialware"&lt;/strong&gt; or "locked&lt;br&gt;
functionality," you didn't do anything unusual — you did the thing every SaaS&lt;br&gt;
tutorial tells you to do, and it's exactly what the directory forbids. Here's the&lt;br&gt;
rule, and the structure that actually passes.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rule that trips everyone up
&lt;/h2&gt;

&lt;p&gt;Guideline 5 says a plugin in the directory can't contain &lt;strong&gt;locked, disabled, or&lt;br&gt;
paywalled&lt;/strong&gt; code. The pattern you reach for by instinct — ship one plugin, grey out&lt;br&gt;
the Pro buttons, gate them behind a license check — is &lt;em&gt;trialware&lt;/em&gt;, and it's an&lt;br&gt;
automatic rejection. It doesn't matter that the code is GPL. If the free download&lt;br&gt;
contains features the user can't use without paying, it's out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The structure that passes: two plugins, not one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free plugin&lt;/strong&gt; lives in the directory. It is &lt;em&gt;fully functional on its own&lt;/em&gt; and
contains &lt;strong&gt;zero&lt;/strong&gt; paid code. No disabled buttons, no dead "upgrade to unlock"
screens wired to features that ship inside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Pro add-on&lt;/strong&gt; is a &lt;em&gt;separate&lt;/em&gt; plugin you sell off-site (Gumroad, Lemon
Squeezy, your own site). When it's installed and licensed, it hooks into extension
points the free plugin exposes and adds the paid features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free plugin never contains the Pro code. The Pro code never ships in the&lt;br&gt;
directory. That's the whole trick, and it's the difference between approved and&lt;br&gt;
rejected.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;The free plugin exposes a filter for its tier and action/filter hooks where an&lt;br&gt;
add-on can attach:&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;// In the FREE plugin. Ships alone =&amp;gt; always false.&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;myplugin_is_pro&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'myplugin_is_pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A gated capability degrades to free behaviour, never to a broken screen.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;myplugin_is_pro&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;do_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'myplugin_render_pro_panel'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;a href="https://your-site.com/pro"&amp;gt;Upgrade to Pro&amp;lt;/a&amp;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the SEPARATE Pro plugin, sold off-site.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'myplugin_is_pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MyPro_License&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'myplugin_render_pro_panel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;MyPro_Feature&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="s1"&gt;'render'&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;Because the free plugin only ever links &lt;em&gt;out&lt;/em&gt; to a sales page and its&lt;br&gt;
&lt;code&gt;myplugin_is_pro()&lt;/code&gt; is false when it ships alone, there's no locked code in the&lt;br&gt;
directory download. The value is real (the free tier works), and the upgrade is a&lt;br&gt;
genuine separate product — which is what the guideline actually wants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four more landmines (all real, all cost time)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;readme.txt is published verbatim.&lt;/strong&gt; Don't advertise features you haven't built
yet — a dead promise on a public plugin page is worse than a missing feature, and
reviewers check the code against the readme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New authors get one plugin in review at a time.&lt;/strong&gt; Queue matters; you can't
parallelise your first few submissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trademark terms are banned in slugs.&lt;/strong&gt; A slug starting with a protected term
(e.g. &lt;code&gt;woocommerce-&lt;/code&gt; / &lt;code&gt;wc-&lt;/code&gt;) gets rejected on name grounds alone. Put the
"for WooCommerce" in the &lt;em&gt;display name&lt;/em&gt;, not the slug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Plugin Check before submitting&lt;/strong&gt; (&lt;code&gt;wp plugin check &amp;lt;slug&amp;gt;&lt;/code&gt;). It catches the
escaping, i18n, and header issues reviewers would bounce you for — fix them first
and you skip a review round-trip.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Skip the boilerplate rebuild
&lt;/h2&gt;

&lt;p&gt;The annoying part is that none of this is your feature — it's the same license gate,&lt;br&gt;
settings framework, wp.org-ready readme, and build-to-zip you rewrite every time. I&lt;br&gt;
packaged the whole compliant structure (plus a one-command scaffolder that stamps out&lt;br&gt;
a namespaced, activation-ready plugin) so you add your feature and ship:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://rebelstudios.gumroad.com/l/xywjzg" rel="noopener noreferrer"&gt;Freemium WordPress Plugin Boilerplate&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
Optional — the structure above is the real lesson, and it's yours whether you build it&lt;br&gt;
by hand or not.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>Why the ATS says you’re “missing” keywords your resume clearly has</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Mon, 14 Sep 2026 20:40:35 +0000</pubDate>
      <link>https://dev.to/rebel_studios/why-the-ats-says-youre-missing-keywords-your-resume-clearly-has-5c10</link>
      <guid>https://dev.to/rebel_studios/why-the-ats-says-youre-missing-keywords-your-resume-clearly-has-5c10</guid>
      <description>&lt;p&gt;Every "ATS keyword" checker tells you the same demoralizing thing: you're &lt;em&gt;missing&lt;/em&gt;&lt;br&gt;
skills you know are on your resume. You wrote "managed a team of six" and it says&lt;br&gt;
you're missing &lt;strong&gt;management&lt;/strong&gt;. You wrote "analyzed sales data" and it flags &lt;strong&gt;data&lt;br&gt;
analysis&lt;/strong&gt; as absent. So you start stuffing exact phrases into your resume to satisfy&lt;br&gt;
a robot — and the result reads like it was written by one.&lt;/p&gt;

&lt;p&gt;Here's the thing: the robot is usually wrong, and it's wrong in a specific,&lt;br&gt;
fixable way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most keyword matchers do dumb string comparison
&lt;/h3&gt;

&lt;p&gt;Under the hood, a lot of these tools check whether the &lt;em&gt;exact&lt;/em&gt; word from the job post&lt;br&gt;
appears in your resume. The job says "management"; your resume says "managed." Those&lt;br&gt;
aren't the same string, so — missing. It doesn't know that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;managed / managing / manager / management&lt;/strong&gt; are the same skill (morphology),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stakeholders&lt;/strong&gt; and &lt;strong&gt;stakeholder&lt;/strong&gt; are the same thing (plurals),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"data analysis"&lt;/strong&gt; and &lt;strong&gt;"analyzed data"&lt;/strong&gt; are the same thing (word order),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt; and &lt;strong&gt;JavaScript&lt;/strong&gt;, &lt;strong&gt;k8s&lt;/strong&gt; and &lt;strong&gt;Kubernetes&lt;/strong&gt; are the same thing (synonyms).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A real ATS (Workday, Greenhouse, Lever) is more forgiving than the cheap checkers —&lt;br&gt;
it does stemming and synonym expansion. So when a checker screams that you're missing&lt;br&gt;
ten keywords, most of those are false alarms, and chasing them makes your resume&lt;br&gt;
&lt;em&gt;worse&lt;/em&gt;, not better.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to actually check your match
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reduce both sides to concepts, not strings.&lt;/strong&gt; For each "missing" keyword, ask:
&lt;em&gt;does my resume show this idea in any form?&lt;/em&gt; If yes, it's a false alarm — leave it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only fix genuine gaps.&lt;/strong&gt; If the job wants "Kubernetes" and you've genuinely never
touched it, no keyword trick fixes that — and you don't want it to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the job's phrasing once, where it's true.&lt;/strong&gt; If you do have the skill but
worded it oddly, align to the job's term a single time. Don't carpet-bomb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write for the human.&lt;/strong&gt; The recruiter reading after the ATS is the real gate. A
keyword-stuffed resume passes the filter and fails the person.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A tool that does the tolerant version
&lt;/h3&gt;

&lt;p&gt;I got annoyed enough at the false "missing" flags that I built a small Chrome&lt;br&gt;
extension that does the tolerant matching — stemming, plurals, word-order, and&lt;br&gt;
common acronyms — so it only flags a skill as missing when your resume genuinely&lt;br&gt;
doesn't show it. Free (3 checks/day), runs entirely on your device, nothing uploaded:&lt;br&gt;
&lt;strong&gt;Resume Keyword Matcher for ATS&lt;/strong&gt; on the Chrome Web Store. Built it for exactly this&lt;br&gt;
problem; sharing in case it saves someone the phrase-stuffing spiral.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If you just want the principle: stop optimizing for string-match robots and start&lt;br&gt;
checking whether the concept is present. That alone fixes most of the panic.)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build small, honest tools at &lt;a href="https://rebelstudiossoftware.com" rel="noopener noreferrer"&gt;rebelstudiossoftware.com&lt;/a&gt;. This one's a free Chrome extension — search "Resume Keyword Matcher for ATS".&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>jobsearch</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Get Your Resume Past an ATS in 2026</title>
      <dc:creator>Rebel Studios</dc:creator>
      <pubDate>Mon, 07 Sep 2026 22:29:38 +0000</pubDate>
      <link>https://dev.to/rebel_studios/how-to-get-your-resume-past-an-ats-in-2026-3225</link>
      <guid>https://dev.to/rebel_studios/how-to-get-your-resume-past-an-ats-in-2026-3225</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://rebelstudiossoftware.com/blog-get-resume-past-ats.html" rel="noopener noreferrer"&gt;rebelstudiossoftware.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your applications keep vanishing into a black hole, the culprit is usually an &lt;strong&gt;Applicant Tracking System (ATS)&lt;/strong&gt; — the software almost every mid-size and large employer uses to receive, parse, and rank resumes before a human sees them. The good news: getting past it is mechanical, not mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an ATS really does (and doesn't)
&lt;/h2&gt;

&lt;p&gt;Kill the biggest myth first: a modern ATS does not usually auto-&lt;em&gt;reject&lt;/em&gt; resumes on its own. It &lt;strong&gt;parses&lt;/strong&gt; your resume into structured fields (name, work history, skills, dates) and makes you &lt;strong&gt;searchable and rankable&lt;/strong&gt; for the recruiter. You get filtered out when your resume either parses badly (so your experience lands in the wrong fields or gets lost) or doesn't match the keywords the recruiter searches for. Both are fixable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two real reasons resumes fail
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parsing failure.&lt;/strong&gt; Columns, tables, text boxes, headers/footers, and graphics confuse the parser, so your data is garbled or dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword mismatch.&lt;/strong&gt; The recruiter searches for the skills in the job description; if your resume uses different words for the same thing, you don't surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to get past it, step by step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mirror the job posting's language.&lt;/strong&gt; If the posting says "CI/CD," don't only write "build pipelines." Use the posting's exact terms where they're truthfully yours — that's what the recruiter searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a clean, single-column format.&lt;/strong&gt; Standard headings ("Experience," "Education," "Skills"), a common font, consistent dates, no graphics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantify.&lt;/strong&gt; "Cut build times 40%," "handled 2M requests/day."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match before you send.&lt;/strong&gt; Compare your resume against each specific posting and fix the missing keywords — the single highest-leverage step, and the one people skip because it's tedious by hand.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What NOT to do
&lt;/h2&gt;

&lt;p&gt;Don't stuff invisible white-text keywords or paste the whole job description into your resume — recruiters see the parsed text and it reads as spam. Don't use a PDF exported from a design tool where the text is an image. And don't send one generic resume to every job; the keyword match is per-posting.&lt;/p&gt;




&lt;p&gt;I got tired of doing step 4 by hand, so I built a free Chrome extension — &lt;strong&gt;Resume Keyword Matcher&lt;/strong&gt; — that reads any job posting, pulls its keywords, and scores how well your resume matches, showing exactly what's missing. Everything runs locally; your resume never leaves your device. &lt;a href="https://rebelstudiossoftware.com/resume-matcher.html" rel="noopener noreferrer"&gt;It's here&lt;/a&gt; if it's useful.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>career</category>
      <category>jobsearch</category>
    </item>
  </channel>
</rss>
