<?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: Fidele Maniraruta</title>
    <description>The latest articles on DEV Community by Fidele Maniraruta (@lmaniraruta).</description>
    <link>https://dev.to/lmaniraruta</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%2F4079646%2F368a1d4d-522a-4b87-b540-d30fd0a69374.jpg</url>
      <title>DEV Community: Fidele Maniraruta</title>
      <link>https://dev.to/lmaniraruta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lmaniraruta"/>
    <language>en</language>
    <item>
      <title>I built a contractor-license Actor that AI agents call and pay for on their own</title>
      <dc:creator>Fidele Maniraruta</dc:creator>
      <pubDate>Thu, 27 Aug 2026 15:24:37 +0000</pubDate>
      <link>https://dev.to/lmaniraruta/i-built-a-contractor-license-actor-that-ai-agents-call-and-pay-for-on-their-own-4e9m</link>
      <guid>https://dev.to/lmaniraruta/i-built-a-contractor-license-actor-that-ai-agents-call-and-pay-for-on-their-own-4e9m</guid>
      <description>&lt;p&gt;I don't have an audience. No newsletter, no Twitter following, no YouTube channel. Every product I shipped before this one died the same way: a human had to discover it, and no humans knew I existed.&lt;/p&gt;

&lt;p&gt;So I flipped the buyer. An AI agent doesn't care about my follower count. It picks tools by spec, reliability, and price — from a registry it can search on its own. If I could ship a tool that agents discover, call, and pay for without a human in the loop, my distribution problem would stop mattering.&lt;/p&gt;

&lt;p&gt;That's what license-verify is: an Apify Actor that verifies a US contractor's license, surety bond, and insurance from official state data, exposed via the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; (MCP) so AI clients like Claude can call it mid-conversation, priced pay-per-event at $0.03 per successful lookup. Here's how I built it, the input-schema decisions that made it agent-callable, and the one-line billing bug that silently made every call free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why contractor licenses
&lt;/h2&gt;

&lt;p&gt;I run a side business building tools for small contractor shops, so I knew the pain firsthand: before a homeowner (or a general contractor, or an insurance adjuster) hires a roofer, someone should check the license is active, the surety bond is real, and the insurance hasn't lapsed. In Washington State, all three live in the Department of Labor &amp;amp; Industries' &lt;a href="https://data.wa.gov/resource/m8qx-ubtq.json" rel="noopener noreferrer"&gt;open-data API&lt;/a&gt; on data.wa.gov. Most tools that "verify licenses" scrape an HTML page and return a status string. The official JSON gives you the actual bond amount and the insurance carrier. That's the difference between "probably fine" and "verified."&lt;/p&gt;

&lt;p&gt;It's also a perfect agent task: a small, well-defined question ("is ECOSTSC758NN licensed, bonded, insured?") with a structured answer an agent can act on. An AI assistant helping someone plan a renovation can reach for it mid-task, the same way it reaches for a calculator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack: one codebase, two doors
&lt;/h2&gt;

&lt;p&gt;The core is a TypeScript verification engine with a provider-per-state design. It ships through two doors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An Apify Actor&lt;/strong&gt; (&lt;code&gt;fidelem/license-verify&lt;/code&gt;, listed on &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;) — this is the monetized surface. Apify handles hosting, billing, and — through the &lt;a href="https://docs.apify.com/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; — exposure to AI clients as a callable tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stdio MCP server&lt;/strong&gt; (&lt;code&gt;license-verify-mcp&lt;/code&gt; on npm) — the same engine wrapped with &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, so anyone can wire it into Claude Desktop or Cursor directly, and it's discoverable on the MCP registries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Apify door is the interesting one for this article, because it's the one where an agent can not only &lt;em&gt;call&lt;/em&gt; the tool but &lt;em&gt;pay&lt;/em&gt; for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the input schema for a caller that can't ask questions
&lt;/h2&gt;

&lt;p&gt;A human user who gets confused reads your README. An agent that gets confused hallucinates an input, gets an error, and moves on to a competitor's tool. Everything about the schema had to assume the caller is a language model seeing it for the first time with zero context.&lt;/p&gt;

&lt;p&gt;Three decisions did most of the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Put the "when to call this" logic in the tool description, not the docs
&lt;/h3&gt;

&lt;p&gt;The description isn't marketing copy — it's the routing signal the agent uses to decide whether your tool fits its current task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verify_license&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Verify a contractor's current license or registration status
before awarding work, signing a contract, or performing due diligence.

Call this tool when you need to:
- Confirm a contractor is currently licensed and in good standing
- Check whether a license is active, expired, suspended, or revoked
- Look up a contractor by license/registration number (preferred — exact match)
  or by business name (partial match, may return multiple)

Currently supports: WA (Washington State) via WA L&amp;amp;I open data (includes real
bond + insurance data); CA (California) via CSLB Check-A-License.
Call list_supported_jurisdictions first if unsure whether a state is supported.`&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;h3&gt;
  
  
  2. Give the agent a cheap "am I in the right place?" tool
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;list_supported_jurisdictions&lt;/code&gt; exists purely so an agent can check coverage before burning a paid call on Alaska. It returns each state's code, data source, and status. Agents actually use it — in my logs, discovery calls precede verification calls constantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Return structured errors, never throw
&lt;/h3&gt;

&lt;p&gt;Every failure mode returns JSON with an error code and a message that tells the agent what to do next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jurisdiction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;provider&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNSUPPORTED_JURISDICTION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`'&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jurisdiction&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' is not currently supported. Call list_supported_jurisdictions to see available options.`&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="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;This mattered immediately for California. CA has no open-data API like WA's, so that provider queries the state's own &lt;a href="https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/CheckLicense.aspx" rel="noopener noreferrer"&gt;CSLB Check-A-License&lt;/a&gt; lookup one license at a time, at normal human-lookup rates — the same page a homeowner would use, never bulk-crawled. It throws intermittent 503s under its own maintenance windows. Early on my CA provider crashed the run; an agent that hits a crashed Actor doesn't retry, it deletes you from its plan. Now a CSLB outage returns a clean structured error, and the agent can decide to retry later or tell the user. Degrade, never die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing for a buyer that might call you 10,000 times
&lt;/h2&gt;

&lt;p&gt;Apify's &lt;a href="https://docs.apify.com/actors/publishing/monetize/pay-per-event" rel="noopener noreferrer"&gt;pay-per-event (PPE)&lt;/a&gt; monetization lets the Actor charge per named event instead of per compute unit. I shipped with one billable event — a successful verification at $0.03 — and made a deliberate call: &lt;strong&gt;no charge on a miss.&lt;/strong&gt; If the lookup finds nothing, the agent pays nothing.&lt;/p&gt;

&lt;p&gt;I later split that single event into two, after watching real runs: plenty of WA lookups return a valid, active license with no bond or insurance record attached (older registrations, license types the bond dataset doesn't cover). Charging full price for a status-only result felt wrong, so now there are two billable events plus the free miss:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"license-verification"&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;"eventTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"License verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"One contractor license lookup returning normalized status (active/expired/suspended/revoked), bond, and insurance data from official state sources."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventPriceUsd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status-only-result"&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;"eventTitle"&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-only result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"License found with status, but no bond or insurance record matched (or caller requested statusOnly). Billed at the reduced rate."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventPriceUsd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For human users this is a nice touch. For agents it changes the economics of the whole category: an agent doing due diligence across a thousand contractors is going to hit plenty of misses and status-only results. Pricing by &lt;em&gt;value returned&lt;/em&gt; — full bond+insurance record vs. bare status vs. nothing — means the agent's cost tracks what it actually got, which makes the tool safe to call speculatively at volume.&lt;/p&gt;

&lt;p&gt;The charge logic lives in the handler, gated on what the result actually contains, using &lt;a href="https://docs.apify.com/sdk/js/reference/class/Actor#charge" rel="noopener noreferrer"&gt;&lt;code&gt;Actor.charge()&lt;/code&gt;&lt;/a&gt; from the Apify SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasBondOrInsurance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bonded&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;is_bonded&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insured&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;has_insurance&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;statusOnly&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasBondOrInsurance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status-only-result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;license-verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which brings me to the bug — one that predates the two-tier split, back when there was only the single &lt;code&gt;license-verification&lt;/code&gt; event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that billed $0: your event name must match the console, exactly
&lt;/h2&gt;

&lt;p&gt;When I set up monetization in the Apify Console, I created the billable event and the console assigned the key &lt;code&gt;license-verification&lt;/code&gt;. In my code, I'd written &lt;code&gt;Actor.charge({ eventName: "verify_license" })&lt;/code&gt; — the name I'd used internally from day one.&lt;/p&gt;

&lt;p&gt;The Actor ran fine. Lookups returned perfect results. Runs showed up in analytics. And every single call billed exactly $0.&lt;/p&gt;

&lt;p&gt;The only symptom was one warning buried in the run log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN Attempting to charge for an unknown event 'verify_license'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd seen that warning during pre-monetization testing and mentally filed it as "expected until monetization is active." Monetization had been active for days. The warning wasn't a leftover — it was the whole problem. Apify doesn't fail the run when you charge an unknown event; it warns and moves on. Reasonable design, brutal failure mode: a "working" Actor that never earns.&lt;/p&gt;

&lt;p&gt;The fix was one line plus a grep to make sure no stale event names survived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"verify_license"&lt;/span&gt; src/   &lt;span class="c"&gt;# find every stale charge string&lt;/span&gt;
npx apify-cli push               &lt;span class="c"&gt;# rebuild + deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a smoke run to confirm the charge resolves with no warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx apify-cli call fidelem/license-verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{"jurisdiction":"WA","license_number":"ECOSTSC758NN"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lesson: &lt;strong&gt;treat the console's event key as the source of truth and paste it into your code, never retype it.&lt;/strong&gt; And read your run-log warnings like they're errors, because for revenue purposes this one was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Becoming agent-eligible: the checklist nobody hands you
&lt;/h2&gt;

&lt;p&gt;Being on Apify doesn't automatically make your Actor something an agent can use end-to-end. Three settings had to line up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PPE pricing&lt;/strong&gt; (not rental, not per-compute) — so a caller can pay per action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited permissions&lt;/strong&gt; — the Actor declares it only touches what it needs; an agent (or the human supervising it) can trust the blast radius.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No standby mode&lt;/strong&gt; — the Actor runs per-call rather than as an always-on server the caller has to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those three in place, the Actor becomes eligible for agentic use — callable and payable without the caller holding an Apify account, which is the piece that makes "an AI agent autonomously pays for a license check" real instead of a demo.&lt;/p&gt;

&lt;p&gt;A real MCP-originated run returns the normalized status plus the actual surety bond (North River Insurance, $30,000) and the real GL policy (State National, $1,000,000, expiring 2027-06-12) — and charges $0.03 for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the traffic looks like when your user isn't a person
&lt;/h2&gt;

&lt;p&gt;The most interesting thing in my analytics isn't the volume — it's the &lt;em&gt;shape&lt;/em&gt;. My steadiest caller runs exactly one verification per day, same account, like clockwork, going on a week straight. No human checks one contractor license every day at the same time. That's a scheduled workflow — someone (or something) wired my Actor into a recurring pipeline.&lt;/p&gt;

&lt;p&gt;That's the quiet promise of this model: you're not chasing installs, you're becoming a dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sweep the store before writing a line of code.&lt;/strong&gt; I searched Apify for competing license-check Actors only after I'd built mine. There were four, one with real traction. My wedge survived — none of them return official bond + insurance data, none are MCP-native, none are agent-payable — but I got lucky. Ten minutes of store search should have been step zero, and the wedge should have been the spec, not a post-hoc discovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship every discovery surface the same day.&lt;/strong&gt; The Actor, the npm package, and the registry listings went live weeks apart. Agents (and the humans configuring them) find tools through registries; every un-listed week is invisible inventory. If I did it again: Apify Store, npm, and the MCP registries in one push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the billable event name first.&lt;/strong&gt; Create the event in the console, copy the key into a constant, and write the charge call around it. Not the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The whole build — provider engine, MCP wrapper, Apify Actor, monetization — was a few evenings of work. The hard parts weren't code. They were caller-empathy problems: writing descriptions for a reader that decides in one pass, returning errors a machine can act on, pricing so speculative calls are safe, and verifying the billing path with the same rigor as the happy path.&lt;/p&gt;

&lt;p&gt;If you've built an Actor, you're one wrapper away from having a tool AI agents can call — and with PPE, one console screen away from getting paid when they do. Check your event names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/lmaniraruta/license-verify-mcp" rel="noopener noreferrer"&gt;github.com/lmaniraruta/license-verify-mcp&lt;/a&gt; · &lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/fidelem/license-verify" rel="noopener noreferrer"&gt;apify.com/fidelem/license-verify&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does the agent need an Apify account to call the Actor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Through the Apify MCP server integration, agentic-eligible Actors (PPE + limited permissions + no standby) can be called and paid for without the caller managing an Apify account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why charge only on success?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents make speculative calls. Pay-per-success aligns cost with value and makes the tool safe to call at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why official open data instead of scraping?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reliability and depth. WA L&amp;amp;I's dataset returns bond amounts and insurance carriers a status page doesn't show — and a JSON API doesn't break when someone redesigns the HTML.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fidele Maniraruta is a Canada-based builder shipping AI-callable tools solo — MCP servers, Apify Actors, and small-business automation for contractor and trade shops. Current work: license-verify (contractor license + bond + insurance verification, WA/CA) and QuoteChaser (AI quote follow-up for contractors). GitHub &lt;a href="https://github.com/lmaniraruta" rel="noopener noreferrer"&gt;@lmaniraruta&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
