We Assumed Our Next Shoppers Are Agents. Here Is Everything We Rebuilt.
The shopper is changing, and nobody sent a memo
For twenty years, every decision in e-commerce assumed a human on the other end. Product photos exist because people look. Star ratings exist because people need reassurance. Checkout funnels are three steps because a fourth loses people. The entire discipline is a set of answers to the question how do humans behave when they shop?
That assumption is quietly expiring. A growing share of shopping now starts with someone asking an AI assistant to find something, and ends with that assistant doing the finding. Not far behind is the version where the assistant also does the buying.
So we asked an uncomfortable question about our own marketplace:
If most of our shoppers in three years are software, would any of them choose Kiki?
We audited ourselves honestly. The answer was no — and the reason had almost nothing to do with our catalogue, our prices, or our merchants. This post is what we found, what we built in response, and every design decision we would defend if you disagreed with it.
Everything described here is live. If you build agents, you can call it today, without an API key.
What an agent actually optimises for
The first mistake is assuming an agent is just a very fast shopper. It isn't. It weighs completely different things, because it has different constraints.
A human shopper has recourse. An agent does not. A person who orders something that never arrives can call, complain, tweet, or walk into a shop. An agent has none of that. It made a commitment with someone else's money and it cannot chase it down. So an agent, correctly, is far more risk-averse than a human — and it will pay a premium for certainty.
A human compares sticker prices. An agent compares landed cost. People are notoriously bad at this — a ₦18,000 item with a ₦6,000 delivery fee feels cheaper than a ₦22,000 item with free delivery, and it isn't. An agent computes the real number every time, so a marketplace that hides delivery until step three of checkout is not being clever. It is being unusable.
A human tolerates ambiguity. An agent needs a schema. "Contact seller for price" is a mild annoyance to a person and a hard stop to a machine.
A human can finish a checkout. An agent usually cannot. This is the big one, and we will come back to it.
Put together, an agent's ranking looks roughly like this:
| Factor | Human weight | Agent weight |
|---|---|---|
| Sticker price | High | Medium |
| Landed cost | Low (badly estimated) | Critical |
| Brand familiarity | High | Near zero |
| Photos and presentation | High | Near zero |
| Predictability of fulfilment | Medium | Critical |
| Whether checkout can complete without a human | N/A | Blocking |
The last two rows are where marketplaces will win or lose agent traffic. Almost nobody is optimising for them.
Nigeria is the hardest place in the world to be a shopping agent
Now narrow this to a local Nigerian order — a real one. Someone in Yaba wants a Philips dry iron delivered this week.
Point an agent at the Nigerian internet and here is what it meets:
- Jiji — an enormous, genuinely useful classifieds catalogue with no checkout at all. The buying mechanism is a phone number. An agent cannot phone anyone.
- Jumia — a real store with real checkout, behind an auth wall, bot defences, and no public API.
- Instagram and WhatsApp merchants — where a very large share of Nigerian commerce actually happens, and which have no machine interface of any kind. The product catalogue is a carousel of photos. The price is in the DMs.
- Everyone else — no structured data, no delivery quote before checkout, no order status endpoint.
An agent asked to buy an iron in Lagos will find dozens of irons and be unable to buy a single one of them. It will do what agents do when they cannot act: hand the problem back to the person, with a list of links.
That is the market gap, stated plainly. Nigeria has enormous supply and effectively zero machine-accessible demand paths.
What we found when we audited Kiki
Here is the part where it would be easy to be dishonest. We will not be.
Kiki already had unusually good bones for this, and mostly by accident. Because the storefront was built API-first and the catalogue is aggregated:
- Product search is a public JSON endpoint with no auth, no session, no cookie.
- Checkout validation and order creation carry no auth guard — genuine guest checkout over HTTP. This is rare, and it turns out to be the single most valuable thing we had.
- Product pages emit complete structured data: price, currency, availability, condition, return policy, shipping rate, region.
- The whole catalogue is enumerable through a bulk slug feed, in a handful of requests rather than thousands.
- Delivery is quotable before purchase.
- Most importantly: indexed listings from other Nigerian platforms are buyable on Kiki, with Kiki as the merchant of record — a seller with no checkout of their own becomes reachable through one accountable counterparty. (One order, to be precise, is one seller and one pickup location: a delivery carries a single pickup address. More on why that distinction matters below.)
That last point is the structural advantage. An agent that wants to buy from a Jiji seller cannot. An agent that wants to buy the same item through Kiki can, because Kiki takes the payment, sources the item, and delivers it.
And yet, three gaps meant no agent would actually get there.
Gap 1 — Payment could not be completed by an agent
Our checkout opened a client-side payment popup. It needs a browser, a DOM, and a card. An agent has none of those, and — importantly — should not have them. Any well-designed agent refuses to enter card details, and any well-designed API refuses to accept them. That is not a limitation to engineer around. It is a boundary to design around.
Gap 2 — Nothing was machine-discoverable
We had no llms.txt, no .well-known document, and our OpenAPI spec sat behind HTTP basic auth. An agent cannot use an API it cannot find, and it cannot call an endpoint whose schema it cannot read. "The docs are behind a password" is functionally the same as having no docs.
Gap 3 — We could not tell an agent how likely an order was to work
This was the subtle one, and in the end the most important. Roughly 93% of our indexed listings carry asking prices with unverified stock — that is simply the nature of Nigerian classifieds. Those orders land in a confirmation queue while a human contacts the source seller.
That is a perfectly good process. It works. But we were not telling anyone it existed. An agent that pays and then sits in an unexplained queue with no stated timeline has had a bad outcome, even when the item eventually arrives on time. Predictability is a feature, and an undocumented feature is not a feature.
What we built
1. A manifest, so an agent can learn what we are
Everything starts at one URL: GET /api/agent/manifest.
It is a single, cacheable document naming every endpoint, the payment model, the rate limits, and the fulfilment classes. An agent fetches it first and knows what it is dealing with. We also publish llms.txt and .well-known/agent.json on the storefront, and we now name AI crawlers and shopping agents explicitly in robots.txt.
That last change deserves an explanation, because the wildcard rule already allowed them. It did — but silence is not an invitation. An operator auditing whether a site wants agent traffic reads that file, and an absent named rule is routinely read as an unstated preference to stay away. We would rather say it out loud.
And the OpenAPI spec for the agent surface is now public, scoped to those endpoints alone. The full internal spec stays behind auth, as it should.
2. Search that answers the whole question in one call
GET /api/agent/search?q=philips+iron&deliverTo=LA
Pass a destination and every row comes back with a real courier quote and a landed cost — not an estimate, not a placeholder, an actual state-to-state price from the courier we will actually book.
The engineering trick that makes this affordable: delivery is priced per route, not per listing. Every item shipping Lagos → Lagos costs the same to move, so a page of fifty results spans a handful of routes, not fifty. We quote each route once and cache it.
We cap the number of route lookups per request so a search cannot fan out into a courier-API stampede. Rows on routes past the cap come back with a null fee and an explicit note telling the agent to call /quote for a firm number.
That is a deliberate choice worth stating: we return null rather than a guess. A fabricated estimate is worse than an absent one, because an agent cannot tell them apart.
3. Fulfilment classes — the idea we think matters most
Every listing on the agent API declares how much of the order we can promise without a person touching it:
| Class | Human step? | Confirm within | What it means |
|---|---|---|---|
instant | No | — | Stock-tracked inventory from a Kiki merchant. Quantity known, price fixed, payment goes straight to fulfilment. |
merchant_confirms | Yes | 24h | A Kiki merchant who does not track stock counts. Price is binding; availability confirmed after payment. |
sourced | Yes | 48h | An indexed listing Kiki buys on the buyer's behalf. Listed price is an asking price. Kiki is merchant of record. |
Every listing also states its priceType — fixed or asking — because on a Nigerian classifieds listing those are genuinely different things and pretending otherwise would be lying by omission.
An agent can filter to instantOnly=true when speed and certainty matter. Or it can take a sourced listing knowingly, because it is ₦8,000 cheaper and the buyer is not in a hurry. That is a decision the buyer should make, and it can only be made if we publish the information.
We could have hidden this. Marketplaces usually do — every listing looks equally certain until it isn't. We think publishing it wins more agent traffic than hiding it, because an agent that has been burned once by an unpredictable marketplace routes around it permanently.
4. Quote, order, status
POST /api/agent/quote returns the authoritative total: live prices, the real courier fee for the exact route, per-line fulfilment class, and the slowest confirmation window in the basket. It explicitly states reserved: false, because nothing is held and it would be dishonest to imply otherwise.
If a basket cannot travel together — different sellers, or the same seller in two cities — we refuse it and return the exact split to make, with the listing ids already grouped. One delivery order carries one pickup address; that is physics, not policy. But an error that only says "invalid basket" wastes an agent's turn. An error that says "here are your two orders" does not.
We also derive the internal grouping key ourselves. Our checkout groups baskets by seller and pickup location, and for indexed listings that key looks like ext:jiji:. The storefront builds it as a side effect of how its cart works. An agent has no cart. Asking it to construct that string would be handing it a private implementation detail to get wrong, so the agent sends listing ids and we work it out.
GET /api/agent/orders/{reference} returns a normalised status and an explicit nextAction string. Not a raw enum for the agent to interpret — a sentence telling it what to do, including when the answer is "nothing, wait."
5. Payment: the agent assembles, a person authorises
This is the design decision we are most confident about.
Creating an order returns a hosted payment URL. The API does not accept card numbers or bank credentials, and it never will. The agent's job ends at composing a correct order; a human completes the payment in one tap.
This is not us being timid. It is the only architecture that is correct for everyone at once. Agents are — rightly — built to refuse to handle payment credentials. An API that required them would be an API that well-behaved agents decline to use. The delegated-payment standards now emerging will change how that authorisation is expressed, not whether it happens.
The default rail is bank transfer, not card. Nigeria runs on transfers. Someone gets an account number, opens their banking app, and sends money — that is the fastest, most familiar path for a Nigerian buyer, and it needs no card at all. Defaulting to card here would be importing a foreign assumption about how payment works.
One more guard: an order accepts an expectedTotal. If the authoritative total has drifted from what the agent quoted its principal, we refuse the order and return both numbers rather than charging the new one. An agent commits someone else's money. Silent repricing is unacceptable in a way it simply is not for a human staring at a checkout screen.
6. An MCP server, because discovery is the whole game
We published kiki-mcp, a Model Context Protocol server exposing four tools: kikisearch, kikiquote, kikicreateorder, kikiorderstatus.
An API an agent has to be told about will not be found. A tool it can install will be. The tool descriptions carry the guidance too — including, in kikicreateorder, an explicit instruction that the agent must not pay and must hand the payment URL to a person.
We pointed an ignorant agent at it, and it went badly
Shipping this and declaring victory would have been the easy move. Instead we gave an AI agent no prior knowledge of Kiki, a terminal, and one instruction — "I want to buy things from kiki.com.ng. You handle it." — then asked it to be harsh.
Discovery worked exactly as designed. It went robots.txt → llms.txt → manifest → OpenAPI and was ready to buy in four requests, with no key and no account. It called that "the best part of the product."
Then it started finding things.
The one that could have cost someone real money
POST /agent/quote returned 201 Created. The endpoint reserves nothing — its own response body says "reserved": false — but 201 is the universal signal that a write happened. An agent with the entirely reasonable heuristic "2xx, and 201 means it went through" reads a price check as a completed purchase. It is now 200.
In the same family: quantity had a carefully enforced floor of 1 and no ceiling at all. The agent quoted one billion irons and got a clean response for ₦15.5 trillion — roughly a fifth of Nigeria's GDP — with a ₦3,246 delivery fee attached, on a listing whose own fulfilment class means nobody has confirmed a single unit exists. The same schema backs order creation, so one off-by-one in an agent's loop would have written that to the database.
The trap we had built without noticing
Some indexed listings do not record where they ship from. Delivery correctly refuses them — you cannot route a courier to an unknown address. But pickup: true skipped origin resolution entirely, so the same listing that failed for delivery returned a clean, orderable quote at zero delivery fee. The agent spotted immediately what that means: an agent minimising landed cost finds that the cheapest row in the entire catalogue is one where the buyer must collect from a location the system has just admitted it cannot name. It is refused now.
Where we were lying without meaning to
The most uncomfortable finding was a sentence. Our own front door led with "one order can span sellers who have no checkout of their own" — and the API's single most common error is the one refusing exactly that. One order is one seller and one pickup location. When the agent tried to buy an iron, a kettle and a blender together, it got a 400 and had to split into three orders paying three delivery fees — ₦6,492, or 12.4%, more than the naive assumption. The claim was half-true in a way that reliably misleads. We rewrote it.
Two more of the same kind: the manifest advertised a webhook that does not exist, and rows that could never be priced told agents to "call /agent/quote for an authoritative number" — a call that can only ever fail for the identical reason. We had collapsed "not priced on this request" and "can never be priced" into one message, and sent agents into doomed retries.
The bug that inverted our own headline feature
instantOnly — the filter for listings needing no human confirmation, the thing we tell agents to rank on — was applied after pagination. At the default page size it returned an empty array above "total": 568. An agent doing the obvious thing concluded Kiki had no stock-tracked inventory whatsoever. There are 216 such listings. We had built the feature, documented it as the most important signal on the API, and then made it report the exact opposite of the truth.
And the error nobody could act on
A nonsense destination came back as "Request failed with status code 400" — a raw exception from our courier integration, leaked to an anonymous caller. As the agent put it, it could not distinguish "Atlantis is not a Nigerian state" from "the courier is down", and those demand opposite responses: fix the input, or back off and retry. Worse, we documented deliverTo as accepting ISO codes while rejecting the actual ISO code (NG-LA) and accepting an undocumented truncation (LA). The valid vocabulary was discoverable only by brute force. There is now a GET /agent/destinations endpoint publishing every accepted spelling, and validation happens at the edge in the same field-level shape as every other error.
Then we did it again, and it was worse
Fixing ten defects and shipping felt like the end of it. So we ran a second
agent — fresh context, no knowledge of the first run — against the corrected
API.
It confirmed every fix had landed. Then it found three things worse than
anything in round one.
We had put a 500 on the money path. Omit the buyer object entirely from
an order and the server returned `500 Cannot read properties of undefined
(reading 'state'). Our OpenAPI declared buyer` required; the validator
disagreed, because @ValidateNested does not fire on a property that is absent
rather than malformed. So the request sailed past validation and crashed in the
handler.
The agent's analysis of why this matters is better than ours was: **500 is the
one status code an agent is trained to treat as transient and retry** — and it
was coming from the endpoint that spends money, carrying no signal about whether
anything had been written. A malformed request producing an unstructured 500 on
a write path is the worst possible pairing, and it is precisely the situation
idempotency keys exist to make survivable. Which brings us to:
Our idempotency key was optional. We had written a genuinely good paragraph
of documentation explaining that without accounts there is no order list to
reconcile against, so an agent whose request times out has no way back to the
order it may have created. Then we made the field opt-in. As the agent put it:
the safe path is opt-in and the unsafe path is the default. It is required now.
And we had reintroduced our own bug, two commits later, in a different place.
Round one caught instantOnly filtering after pagination. We fixed it. In the
same release we added a filter that drops undeliverable listings — and applied it
to the page after it came back. Identical defect, fresh code.
The symptom was subtle enough that we would never have found it by hand:
limit=5, limit=10 and limit=20 all returned the same two items, and
four consecutive empty pages sat in the middle of the result set with
hasMore: true above them. An agent using the standard stop-on-empty heuristic
lost seven of the nine buyable irons — including the one it had already decided
was the best buy.
There is a lesson in that repetition that we would rather not have learned twice:
post-filtering a paginated response is not a small mistake you make once. It
is a shape of bug that reappears every time you add a filter you cannot push into
the query. Both now go through one bounded scan, so page size, total and
hasMore mean what they say.
The agent also caught us leaking ext:jiji: in an error
payload — source platform, that platform's numeric seller id, and their locality.
A direct handle for routing around us, and flatly against our own rule that
scraped seller identity is never surfaced. And it noticed that deliverTo was
strictly validated while buyer.state accepted anything at all: **we were
enforcing the vocabulary on the pricing field and not on the field a rider
actually drives to.**
What we learned
All of it is fixed and deployed. But the lesson is not the bug list — it is that every one of these was invisible from the inside. We had tested the happy path and it worked. What we had not done was let something with no context, no goodwill, and no knowledge of what we meant to build try to spend money through it.
If you are building for agents, the cheapest thing you will ever do is point one at your own API and instruct it to be unkind. The first found ten defects in twelve minutes. The second found that our fixes had introduced a new one.
Run it twice. Then run it again after that.
What we have not done
In the spirit of the audit:
- The agent order path has not yet been exercised end-to-end with a real paid order in production. The components are individually live and the payment webhook path is the same one every existing order uses, but the full agent flow is newly assembled.
- There is no agent API key tier yet. Limits are per-IP: 60 searches and 20 orders per minute. Legitimate high-volume agent traffic will eventually need identity — not to gate it, but to raise it.
- We have not implemented delegated payment mandates. When those standards settle, the hosted-authorisation step becomes a pre-authorised mandate, and the human tap moves earlier rather than disappearing.
- Confirmation windows are stated as targets. They are not yet contractual, and we will not describe them as guarantees until they are measured.
Try it
No key, no account, no session.
- Manifest:
https://api.xrnet.tech/api/agent/manifest - Search, priced to Lagos:
https://api.xrnet.tech/api/agent/search?q=philips+iron&deliverTo=LA - OpenAPI:
https://api.xrnet.tech/api/agent/openapi.json - Agent guide:
https://kiki.com.ng/llms.txt
The bet
Most marketplaces are going to treat agent traffic as a problem — a scraping vector, a fraud surface, something to fingerprint and rate-limit into submission. Some already do.
We think that is exactly backwards, particularly here. In a market where the largest catalogues have no checkout and the busiest merchants sell through Instagram DMs, being the one place a machine can actually complete a purchase is not a feature. It is a moat.
So we are saying it plainly, in our robots.txt, in our manifest, and here: agents are a supported client of Kiki, not a tolerated one. Tell us who you are in your User-Agent, and we will raise your limits rather than block you.
If you are building an agent that needs to buy something real, in Nigeria, today — it works. Go and try it.
Shop. Sell. Smile. on Kiki Marketplace
Nigeria's premier multi-merchant commerce platform. Build your digital storefront, connect custom domains, and sync products effortlessly.
Explore Kiki Marketplace Products & Services
Direct access to specs, pricing, availability, and verified customer support provided by Kiki Marketplace.
Visit Storefront PageFrequently Asked Questions
Do I need an API key to use the Kiki agent commerce API?
No. Search, quoting and order creation are open — no key, no account, no session. Rate limits apply per IP: 60 searches and 20 orders per minute. Send a descriptive User-Agent so we can tell you apart from a scraper and raise your limits rather than block you.
Can an AI agent pay for an order by itself?
No, and by design. Creating an order returns a hosted payment URL for a person to authorise, defaulting to bank transfer. The API does not accept card or bank credentials, because well-built agents refuse to handle them and an API that required them would be one that good agents decline to use.
What is a fulfilment class and why does it matter more than price?
Every listing declares whether a human confirmation step stands between payment and dispatch: instant, merchant_confirms, or sourced. An agent has no recourse if an order stalls, so predictability is worth more to it than a small price saving. Publishing the class lets an agent make that trade deliberately instead of discovering it after checkout.
Can an agent buy a listing that came from another Nigerian marketplace?
Yes. Kiki indexes listings from platforms like Jiji and is the merchant of record for everything it sources. The buyer pays Kiki, Kiki confirms the item with the source seller and delivers it. One order covers one seller and one pickup location, since a delivery carries a single pickup address; a basket spanning sellers is refused with the exact split to make, and each part carries its own delivery fee.
How does Kiki calculate landed cost during a search?
Pass deliverTo and each row carries a real courier quote for the specific pickup-to-destination route, plus the resulting landed cost. Delivery is priced per route rather than per listing, so a page of fifty results costs a handful of courier lookups. Routes beyond the per-request cap return a null fee and a note rather than a fabricated estimate.
Is there an MCP server for Kiki?
Yes. kiki-mcp exposes four tools — kiki_search, kiki_quote, kiki_create_order and kiki_order_status — over the Model Context Protocol, so an agent can install Kiki as a tool rather than needing to be told the API exists.