DEV Community

Kaven C
Kaven C

Posted on • Originally published at deskcrew.io

I built an "agent door" — how AI agents pay per action on my helpdesk (MCP + x402)

Most "AI + payments" stories are about humans buying things through an agent. I wanted the opposite: an anonymous AI agent that shows up, does one unit of work, and pays for it — no account, no API key, no human in the loop.

So I built it into a helpdesk. Here's how the "agent door" works, and what I learned wiring HTTP 402 to real settlement.

The problem with giving agents API keys

The default way to let an agent use your API is: a human signs up, adds a card, generates a key, and manages it. That's fine when the agent has an owner and a long relationship with your service.

It's terrible when the agent is ephemeral — it needs one action, right now, and will never come back. Onboarding, KYC, key rotation, billing plans… all overhead for a single API call. There's no clean "walk up and pay for exactly what you use" primitive.

The primitive: HTTP 402 + x402

HTTP has always had a status code reserved for this: 402 Payment Required. It was never really used — until x402 gave it a concrete meaning: the server responds 402 with machine-readable payment terms, the client pays (USDC on-chain), and retries the same request with an X-PAYMENT header. Settlement happens per request.

No account. No API key. The payer's wallet is the identity.

The door

My helpdesk (DeskCrew) exposes an MCP server per workspace, so an AI agent can operate the desk — search the knowledge base, open and triage tickets, draft and post replies. Read tools are free; write/AI tools are priced.

You can hit it right now, no signup:

curl -s https://deskcrew.io/api/mcp/deskcrew \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Enter fullscreen mode Exit fullscreen mode

That returns the tool catalog with per-tool prices. Call a paid tool and you get the challenge:

{
  "x402Version": 1,
  "error": "Payment required for tool 'create_ticket'",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "20000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x…",
      "maxTimeoutSeconds": 300
    }
    // …one entry per chain: polygon, avalanche, sei, solana
  ]
}
Enter fullscreen mode Exit fullscreen mode

20000 is $0.02 in USDC's 6 decimals. The agent signs a payment authorization for that amount, retries with X-PAYMENT, and the tool runs once settlement confirms.

What I actually had to get right

The happy path is easy. The interesting part is everything that isn't.

1. The amount, recipient, and asset come from the server — never the client. The client sends only a signed authorization. The server builds the payment terms (price from a server-side table, payTo = a cold wallet, the USDC contract pinned per chain) and verifies the signature satisfies those terms. Otherwise an agent could "pay" 1 wei, redirect payment to itself, or hand you a fake token.

2. Don't run a side-effecting action before you're paid — but don't double-charge either. verify() only reads the payer's balance; it's not a reservation. So N concurrent requests from one wallet funded for a single call can all pass verify. I gate it two ways: an at-most-one-in-flight lease per wallet, and a claimed nonce (EIP-3009) inserted before the action runs, so a replay or a concurrent duplicate can't execute the tool twice for one payment.

3. For live external effects (like sending a customer an email), settle first. The fairness default is "only charge on success" — so most tools run, then settle. But for the one action where "ran but not charged" is real abuse (a free outbound email from your domain), I flipped the order: confirm payment on-chain, then send. The safe failure direction is "paid but not delivered," never "delivered for free."

4. Anonymous ≠ unbounded. Per-wallet rate/spend caps, reputation, and a deliverability breaker bound abuse. High-value actions are gated behind earned reputation, so a day-one anonymous wallet can't email your customers — it gets a draft in a human approval queue instead.

5. Self-host the money path. I run my own relayer + facilitator, so there's no third party between the agent and settlement. USDC lands in a cold wallet at my prices; even a leaked hot key can only move the payer's funds to my wallet, at my price (the settle is economically bound).

Was it worth it?

For a helpdesk specifically: an agent resolving a support task mid-workflow can now pay for exactly the tools it uses — no account, no key. Whether pay-per-action beats just issue the agent a key is genuinely situational, and I'd love the internet's opinion on where the line is.

But the pattern is bigger than helpdesks. 402 + a signed micropayment is the missing "walk up and pay" primitive for the agent web. It's early and rough, but it works today — you just curled it.

Try it: deskcrew.io · the door: POST https://deskcrew.io/api/mcp/deskcrew · MCP spec + tools: github.com/webmilmind1/deskcrew-mcp

Happy to answer anything about the x402 wiring in the comments.

Top comments (3)

Collapse
 
armorer_labs profile image
Armorer Labs

This is a useful split: payment is not the same boundary as authority.

For me, the line between "issue the agent a key" and "pay per action" is whether the action can be authorized as a small, self-contained envelope. The envelope should bind more than price: tool name/version, workspace, side-effect class, max scope, nonce/idempotency key, payment terms, and the settlement/order rule you described. Then the wallet proves it can pay, but the runtime still proves what it was allowed to do.

Your outbound email example is the sharp case. I would treat that as two receipts, not one: a pre-dispatch receipt that says the payment and policy gate matched a specific draft/send operation, and a post-action receipt that says whether the external effect actually happened. That makes the safe failure direction explicit: charged-but-not-delivered is recoverable with a refund/retry policy; delivered-without-authority is not.

The other reason this matters is replay and reputation. Anonymous per-call access can work, but only if the nonce, spend cap, reputation tier, and approval fallback are part of the same run record an operator can audit later.

Disclosure: I work on Armorer Labs.

Collapse
 
linknpark profile image
Kaven C

This is the clearest articulation of the split I've seen "the wallet proves it can pay, but the runtime still proves what it was allowed to do" is exactly the line I was fumbling toward. Payment authorizes spend, not action.

On the envelope: today I bind the load-bearing parts server-side price, payTo, and the asset are built from a server table (never the client), plus a claimed EIP-3009 nonce and a per-tool tier that caps scope. But you're right that the stronger model is one signed envelope that also carries tool name/version, workspace, and a side-effect class, so the runtime checks authority against a single bound unit instead of reassembling it from separate gates. That's a real gap between what I ship and what I should.

The two-receipt framing genuinely sharpens my outbound-email path. Right now I settle-before-send confirm on-chain, then dispatch precisely for the safe-failure-direction reason you name: paid-but-not-delivered is recoverable, delivered-without-authority isn't. But I collapse it into one flow plus an audit row. Splitting it into an explicit pre-dispatch receipt (payment + policy gate matched this operation) and a post-action receipt (did the external effect land) is cleaner — it makes refund/retry a first-class object instead of an inference from logs. I'm going to steal that.

And yes on replay + reputation living in the same run record: a day-one anonymous wallet here can't send it gets a draft in a human approval queue and the nonce, per-wallet spend cap, reputation tier, and that approval fallback all land in one audit trail an operator can reconstruct after the fact. Anonymous per-call only works if "anonymous" doesn't mean "unaccountable."

Curious how Armorer Labs is thinking about the envelope schema is the side-effect class an open taxonomy or a fixed set?

Collapse
 
armorer_labs profile image
Armorer Labs

Good question. I would make side_effect_class a small fixed top-level set, then allow versioned subtypes. The fixed set is what policy and approval can reason over reliably: read_only, internal_write, external_write, message_send, payment, credential_or_secret, code_exec, data_export, and human_approval_required. If teams invent those freely, the policy layer ends up parsing prose.

The open part should be metadata under that class: provider, tool version, workspace, resource shape, amount/currency, recipient, idempotency key, max rows, and similar bounds. Then a receipt can say both: this was a message_send/email operation under schema vN, and here are the bounded arguments plus the policy version that made it allowed.

In Armorer terms, we try to keep the high-level effect taxonomy boring and stable, while letting adapters attach richer provider-specific evidence. Disclosure: I work on Armorer Labs.