Skip to content
FLAVIO COPES
flaviocopes.com

I built an MCP server to buy Cloudflare domains safely

By

I built a guarded MCP server that lets an AI search, quote, and buy Cloudflare domains only after an exact human approval.

~~~

Today I launched Cloudflare Domains Toolkit on Prototyped.

It lets an AI search for domains, check the real Cloudflare price, prepare a quote, and buy one after you approve the exact charge.

I used it to buy hostingpicker.dev for $12.20.

The API call that bought the domain was the easy part.

The interesting part was building a boundary around it.

Domain registration is billable and non-refundable. I did not want the AI to decide that a purchase was probably what I meant.

So I wrote one rule before writing the server:

The AI can search and prepare. A person approves the exact live purchase.

I also added a hard $20 safety limit. This is separate from the $20 price of the prototype. The server limit applies to the complete initial domain charge.

Here is how I built it.

I split registration and DNS into two tools

Domain registration and DNS management are related, but they have different risks.

The Registrar MCP server has four tools:

The companion cf-domains command-line tool handles DNS:

This keeps the billable surface small.

The Registrar token cannot edit DNS. The DNS token cannot buy domains.

It also makes the MCP server easy to understand. Four tools are enough.

I started with the Cloudflare boundary

I put every Registrar HTTP request behind one TypeScript interface:

interface RegistrarClient {
  checkDomain(domain: string): Promise<DomainAvailability>
  searchDomains(query: string, limit: number): Promise<DomainAvailability[]>
  getMinimumRegistrationYears(domain: string): Promise<number>
  createRegistration(
    domain: string,
    years: number
  ): Promise<RegistrationWorkflow>
}

The rest of the program does not know about Cloudflare URLs, authentication headers, or response envelopes.

This gave me a clean place to contain API changes.

More importantly, it let the tests use a fake Registrar client. The policy tests never contact Cloudflare and can never buy a domain.

A purchase starts with one exact domain

The purchase tool does not guess what the user meant.

It accepts a plain ASCII domain such as:

hostingpicker.dev

It rejects URLs, paths, ports, wildcards, Unicode input, empty labels, and bad hyphen placement.

Search can be loose because it is for discovery. Quote and purchase work with one exact registrable domain.

That distinction matters. An AI can be creative while suggesting names. It should become boring before money is involved.

I represented money as integer cents

Cloudflare returns decimal price strings.

The server parses them into integer cents before applying any rule. There are no floating-point comparisons around the $20 limit.

Some domain extensions require more than one year. For those domains the first charge is:

registration price + renewal price × remaining required years

The server calculates the complete first charge, then compares it with the limit.

A domain advertised below $20 can therefore still be rejected if its minimum term makes the initial bill larger.

I also reject premium domains and prices outside USD. Those cases make the approval harder to explain and easier to misunderstand.

A quote is a temporary decision record

The purchase tool cannot receive an arbitrary domain and amount.

First, quote_domain_purchase asks Cloudflare for authoritative availability and pricing. It reads the minimum registration term, calculates the complete initial charge, and stores a quote in memory.

The quote contains:

Quotes expire after five minutes. They are single-use and disappear when the MCP process stops.

A quote is not a reservation. It is a short-lived record of the decision the user is about to make.

The server checks everything again

Five minutes is short, but domain availability and prices can still change.

When purchase_domain runs, the server checks that the caller repeated the quote ID, domain, currency, and total cents.

Then it asks Cloudflare for availability, price, and minimum term again.

If anything changed, the purchase stops. The user must create a new quote.

Only after this live recheck does the server show the approval form.

The person approves the latest known charge, not a number cached a few minutes earlier.

MCP elicitation provides the human approval

The final decision happens through MCP elicitation.

The form repeats the domain, exact USD total, registration term, non-refundable warning, and disabled auto-renewal state.

The checkbox defaults to false. The purchase continues only when the client returns an accepted form with the value set to true.

If the MCP client does not support elicitation, the server blocks the purchase. There is no terminal fallback or environment flag that skips approval.

This is the real approval I used to buy hostingpicker.dev:

The MCP approval form for buying hostingpicker.dev for $12.20 through Cloudflare Registrar

The screenshot shows where policy becomes interface.

The code can enforce the boundary, but the person still needs to see one clear decision.

I found a concurrency bug before the real purchase

My first version marked a quote as used after waiting for approval.

That worked in a normal sequence. It was unsafe when two purchase calls arrived close together.

Both calls could reach the approval step before either one consumed the quote. The client could show two forms for one quote and potentially send two registration requests.

I changed the order:

quote.used = true
const approved = await approve(publicQuote(quote))

The quote is now consumed before the asynchronous approval begins.

A focused test opens the first approval, starts a second purchase with the same quote, and confirms that the second call fails immediately.

The general lesson is simple: consume single-use state before yielding control.

An ambiguous network result is not retried

The server never retries a registration automatically.

Imagine that the request reaches Cloudflare, but the connection fails before the response comes back.

The server cannot know whether the domain was bought. Retrying an uncertain, billable action could make the situation worse.

The quote remains used. The person must inspect the Cloudflare account before trying anything else.

This rule applies to many payment and provisioning APIs: never retry an ambiguous destructive request unless the API gives you a safe idempotency mechanism.

I tested the rules before touching a real account

The tests use fake Cloudflare clients and placeholder credentials.

They cover:

A smoke test also starts the compiled MCP server, performs a real protocol handshake, and discovers all four tools.

The DNS CLI tests inspect generated requests through a fake fetch. They do not change real DNS.

The goal was not a large test count. It was evidence for every important safety claim.

I ran one real purchase last

I kept all live testing read-only until the local policy tests passed.

Then I connected the MCP server to my Cloudflare account and ran the complete flow:

  1. Search for domain ideas.
  2. Check hostingpicker.dev against the registry.
  3. Create a five-minute quote.
  4. Confirm the $12.20 price and one-year term.
  5. Open the MCP approval form.
  6. Approve the purchase with auto-renewal disabled.
  7. Submit one registration request.

The real run proved something the unit tests could not: the client displayed the approval clearly at the exact moment the billable action was ready.

What the prototype includes

Cloudflare Domains Toolkit is available on Prototyped for a one-time $20 purchase.

The download includes:

You need Node.js 20 or newer, a Cloudflare account with billing and a default registrant contact, and two narrowly scoped API tokens.

The reusable idea is bigger than domain registration:

  1. Separate discovery from action.
  2. Record the exact decision for a short time.
  3. Recheck external state immediately before approval.
  4. Show the latest values to a person.
  5. Require an explicit protocol-level decision.
  6. Consume single-use state before awaiting anything.
  7. Never automatically retry an ambiguous destructive call.
  8. Test the policy with fakes before one real end-to-end run.

The API integration is the easy part. The product is the boundary around it.

Get Cloudflare Domains Toolkit on Prototyped.

Tagged: News · All topics
~~~

Related posts about news: