DEV Community

Fidele Maniraruta
Fidele Maniraruta

Posted on

I built a contractor-license Actor that AI agents call and pay for on their own

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.

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.

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 Model Context Protocol (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.

Why contractor licenses

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 & Industries' open-data API 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."

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.

The stack: one codebase, two doors

The core is a TypeScript verification engine with a provider-per-state design. It ships through two doors:

  1. An Apify Actor (fidelem/license-verify, listed on Apify Store) — this is the monetized surface. Apify handles hosting, billing, and — through the Apify MCP server — exposure to AI clients as a callable tool.
  2. A stdio MCP server (license-verify-mcp on npm) — the same engine wrapped with @modelcontextprotocol/sdk, so anyone can wire it into Claude Desktop or Cursor directly, and it's discoverable on the MCP registries.

The Apify door is the interesting one for this article, because it's the one where an agent can not only call the tool but pay for it.

Designing the input schema for a caller that can't ask questions

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.

Three decisions did most of the work.

1. Put the "when to call this" logic in the tool description, not the docs

The description isn't marketing copy — it's the routing signal the agent uses to decide whether your tool fits its current task:

{
  name: "verify_license",
  description: `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&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.`,
  ...
}
Enter fullscreen mode Exit fullscreen mode

2. Give the agent a cheap "am I in the right place?" tool

list_supported_jurisdictions 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.

3. Return structured errors, never throw

Every failure mode returns JSON with an error code and a message that tells the agent what to do next:

const provider = providers.get(jurisdiction);
if (!provider) {
  return {
    content: [{
      type: "text" as const,
      text: JSON.stringify({
        error: "UNSUPPORTED_JURISDICTION",
        message: `'${jurisdiction}' is not currently supported. Call list_supported_jurisdictions to see available options.`,
      }),
    }],
  };
}
Enter fullscreen mode Exit fullscreen mode

This mattered immediately for California. CA has no open-data API like WA's, so that provider queries the state's own CSLB Check-A-License 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.

Pricing for a buyer that might call you 10,000 times

Apify's pay-per-event (PPE) 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: no charge on a miss. If the lookup finds nothing, the agent pays nothing.

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:

[
  {
    "license-verification": {
      "eventTitle": "License verification",
      "eventDescription": "One contractor license lookup returning normalized status (active/expired/suspended/revoked), bond, and insurance data from official state sources.",
      "eventPriceUsd": 0.03
    }
  },
  {
    "status-only-result": {
      "eventTitle": "Status-only result",
      "eventDescription": "License found with status, but no bond or insurance record matched (or caller requested statusOnly). Billed at the reduced rate.",
      "eventPriceUsd": 0.01
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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 value returned — 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.

The charge logic lives in the handler, gated on what the result actually contains, using Actor.charge() from the Apify SDK:

if (result.found) {
  const records = result.result ? [result.result] : (result.matches ?? []);
  const hasBondOrInsurance = records.some(
    (r) => r.bonded?.is_bonded || r.insured?.has_insurance
  );
  const statusOnly = input?.statusOnly === true || !hasBondOrInsurance;

  if (statusOnly) {
    await Actor.charge({ eventName: "status-only-result" });
  } else {
    await Actor.charge({ eventName: "license-verification" });
  }
}
Enter fullscreen mode Exit fullscreen mode

Which brings me to the bug — one that predates the two-tier split, back when there was only the single license-verification event.

The bug that billed $0: your event name must match the console, exactly

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

The Actor ran fine. Lookups returned perfect results. Runs showed up in analytics. And every single call billed exactly $0.

The only symptom was one warning buried in the run log:

WARN Attempting to charge for an unknown event 'verify_license'
Enter fullscreen mode Exit fullscreen mode

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.

The fix was one line plus a grep to make sure no stale event names survived:

grep -rn "verify_license" src/   # find every stale charge string
npx apify-cli push               # rebuild + deploy
Enter fullscreen mode Exit fullscreen mode

Then a smoke run to confirm the charge resolves with no warning:

npx apify-cli call fidelem/license-verify \
  --input '{"jurisdiction":"WA","license_number":"ECOSTSC758NN"}'
Enter fullscreen mode Exit fullscreen mode

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

Becoming agent-eligible: the checklist nobody hands you

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

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

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.

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.

What the traffic looks like when your user isn't a person

The most interesting thing in my analytics isn't the volume — it's the shape. 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.

That's the quiet promise of this model: you're not chasing installs, you're becoming a dependency.

What I'd do differently

Sweep the store before writing a line of code. 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.

Ship every discovery surface the same day. 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.

Design the billable event name first. Create the event in the console, copy the key into a constant, and write the charge call around it. Not the other way.

Conclusion

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.

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.

Repo: github.com/lmaniraruta/license-verify-mcp · Actor: apify.com/fidelem/license-verify

FAQ

Does the agent need an Apify account to call the Actor?

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.

Why charge only on success?

Agents make speculative calls. Pay-per-success aligns cost with value and makes the tool safe to call at scale.

Why official open data instead of scraping?

Reliability and depth. WA L&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.


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 @lmaniraruta.

Top comments (0)