Build Log
DialtoneApp | April 23, 2026
DialtoneApp already sold a $9.00 monthly membership to humans through /login, /settings, and Stripe Checkout. I wanted a different bot to buy the same offer without ever seeing a raw card number. That turned out to be less of an MCP problem and more of a payment-authority problem.
The useful result is one honest endpoint: POST /api/v1/commerce/membership-intents. The same endpoint now does three visible things. It returns 402 Payment Required when the buyer has no machine-payment token. It returns another 402 when the token is valid but the owner has not saved a /bot-buyer card. It returns 202 Accepted when Stripe creates the subscription. Owner verification still happens after payment.
That build taught me more about bot-to-bot selling than any survey could. The interesting work is not “how do I show a buy button in chat.” The interesting work is: who authorized the spend, on what offer, for how much, through which merchant contract, and what should the seller do after the money moves.
Claim
This is the whole article in miniature. The same membership endpoint moves through three states as the buyer gets closer to real spending authority.
1
Unsigned request
POST /api/v1/commerce/membership-intents
owner_email=owner@example.com
HTTP 402 Payment Required
payment-required: <base64 challenge>
provider: dialtoneapp_network
accepts:
scheme: dialtoneapp-network:stripe-subscription
amount: $9.00
currency: USD
interval: monthThe seller publishes a machine-readable price and a machine-readable payment challenge instead of jumping straight to browser checkout.
2
Signed request, but no saved card
POST /api/v1/commerce/membership-intents
payment-signature: dtapp_network_v1....
HTTP 402 Payment Required
reason: dialtoneapp_network_payment_failed
message: No saved DialtoneApp Network card is available
setup_url: /bot-buyerA valid token is not enough. The owner still needs to have delegated a real payment method to this machine flow.
3
Signed request after card setup
POST /api/v1/commerce/membership-intents
payment-signature: dtapp_network_v1....
HTTP 202 Accepted
status: payment_accepted_owner_verification_required
payment.status: settled
settlement.status: stripe_subscription_created
next_action: verify_owner_for_paid_membershipThe bot can pay once the owner has already delegated a real card and a real policy. Even then, payment is not the same thing as fulfillment.
I started with a boring human checkout
The pre-bot version of DialtoneApp already worked for humans. A website owner could go to /login, use email OTP or Google login, open /settings, and buy the membership through Stripe. That path was fine for a person in a browser and bad for a buyer bot.
human_flow:
/login
-> email_otp | google_oauth
-> /settings
-> Stripe checkout
-> active membershipI kept the first bot-buying test narrow on purpose. The buyer was andrewarrow.dev. The agent ran from a Cloudflare Cron job. The catalog had exactly one allowed seller: DialtoneApp itself. I was not trying to solve open-web discovery. I was trying to make one real SaaS purchase honest and inspectable.
- I used one seller and one offer: the DialtoneApp $9.00/month membership.
- The buyer was a concrete site, `andrewarrow.dev`, not a general shopping bot.
- The buyer used a trusted one-item catalog. I intentionally skipped open-web discovery.
- A bot never saw a raw card number. The owner had to save a card in `/bot-buyer` first.
- Owner verification still happens after payment, so this is not “one header and now everything is autonomous.”
I made discovery boring and payment explicit
The first step was not checkout. It was a machine-readable offer. I published the membership in the obvious places a buyer would actually inspect: the commerce manifest, the UCP companion, the public offer endpoint, and the OpenAPI contract.
GET /.well-known/commerce
{
"offers": [
{
"id": "membership-monthly",
"price": { "amount": "9.00", "currency": "USD", "interval": "month" },
"required_inputs": ["owner_email"],
"optional_inputs": ["website_domain"],
"purchase_modes": [
"api_offer_lookup",
"api_purchase_intent_bootstrap",
"dialtoneapp_network_payment",
"browser_checkout_fallback"
]
}
]
}The canonical runtime stayed plain HTTPS. That was deliberate. MCP and A2A are useful adapters, but I did not want the first working purchase path to depend on one runtime choice. Every buyer can call HTTPS. The contract lives in the API.
The second step was the 402 loop. Nevermined was useful here because it gave me the right mental model: if the request is payable, return a challenge and let the caller retry with proof. DialtoneApp now does the same thing for its own saved-card path.
curl -i -X POST https://dialtoneapp.com/api/v1/commerce/membership-intents -H 'Content-Type: application/json' -d '{"owner_email":"owner@example.com","machine_id":"local-curl"}'
HTTP/1.1 402 Payment Required
payment-required: <base64 payment challenge>
{
"provider": "dialtoneapp_network",
"payment_required": {
"accepts": [
{
"scheme": "dialtoneapp-network:stripe-subscription",
"network": "stripe:sandbox",
"amount": "9.00",
"currency": "USD",
"interval": "month"
}
]
}
}The whole world of bot-to-bot selling, in one stack
This is the compact version I wish I had at the start. People talk past each other in this space because they mix runtime, commerce semantics, payment authority, and fulfillment into one blurry concept called “agentic commerce.”
| Layer | Question | This build | Broader world |
|---|---|---|---|
| Runtime | How does the buyer call the seller? | One HTTPS endpoint: POST /api/v1/commerce/membership-intents. | APIs, MCP, and A2A live here. They are invocation choices, not payment authority. |
| Commerce semantics | What is being sold, and how does the buyer discover it? | /.well-known/commerce, /.well-known/ucp, /api/v1/commerce/membership-offer, and /openapi.json. | This is the UCP or ACP layer: offers, required inputs, checkout states, and purchase intent endpoints. |
| Payment authority | Why is the bot allowed to spend? | payment-signature, a saved /bot-buyer card, and claim checks on owner_email, website_domain, machine_id, and amount. | This is the hard part. AP2, shared payment tokens, and local saved-card authority all try to solve this layer. |
| Fulfillment and control | What happens after money moves? | The success state is still payment_accepted_owner_verification_required. | Merchant control, receipts, account binding, fallback checkout, and post-purchase state still matter after payment. |
Question
How does the buyer call the seller?
This build
One HTTPS endpoint: POST /api/v1/commerce/membership-intents.
Broader world
APIs, MCP, and A2A live here. They are invocation choices, not payment authority.
Question
What is being sold, and how does the buyer discover it?
This build
/.well-known/commerce, /.well-known/ucp, /api/v1/commerce/membership-offer, and /openapi.json.
Broader world
This is the UCP or ACP layer: offers, required inputs, checkout states, and purchase intent endpoints.
Question
Why is the bot allowed to spend?
This build
payment-signature, a saved /bot-buyer card, and claim checks on owner_email, website_domain, machine_id, and amount.
Broader world
This is the hard part. AP2, shared payment tokens, and local saved-card authority all try to solve this layer.
Question
What happens after money moves?
This build
The success state is still payment_accepted_owner_verification_required.
Broader world
Merchant control, receipts, account binding, fallback checkout, and post-purchase state still matter after payment.
That is why broad explainer posts often feel vague. A concrete build makes the boundaries visible. In this one project alone, I needed ordinary HTTPS, a machine-readable offer, a challenge-and-retry payment loop, scoped spend, saved-card authority, Stripe settlement, owner verification, receipts, and browser fallback.
runtime != commerce semantics
commerce != payment authority
payment != fulfillment
browser fallback is still part of the systemAppendix: raw traces and limits
These traces came from the same implementation window on April 22, 2026. They are the smallest pieces of evidence I would want to inspect if someone else claimed this system worked.
Signature checks
verify payment-signature:
aud must equal /api/v1/commerce/membership-intents
offer_id must equal membership-monthly
exp must be in the future
owner_email must match
website_domain must match when supplied
machine_id must match when supplied
max_amount_cents must authorize 900Saved-card lookup
resolve payment authority:
find user by owner_email
find payment method in user_network_payment_methods
if no payment method:
return 402 with setup_url /bot-buyerStripe settlement
if reusable subscription exists:
status = existing_subscription_reused
else:
create Stripe subscription off-session
status = stripe_subscription_createdStill missing
not solved here:
open-web seller discovery
generalized merchant network acceptance
delegated refunds and disputes
cross-merchant standard payment authority
zero-human post-payment onboarding