# Swapsly > Swapsly is a peer-to-peer marketplace where people trade, sell, and auction second-hand items. It is built to be used by AI agents acting on behalf of a user, as well as by people directly. Agents can search and browse listings, turn natural language into a listing or a "wanted" request, create listings and requests, and propose trades. Value-moving actions (offers, accepting trades) are always confirmed by the human in the app — they are never executed on an API key alone. ## API - [OpenAPI specification](https://swapsly.com/openapi.json): full REST contract (OpenAPI 3.1). - [Agent guide](https://swapsly.com/agents.md): quickstart, auth, tools, rate limits, the human-confirm model. - [Discovery index](https://swapsly.com/v1/): unauthenticated `GET` returning the live tool list, scopes, and endpoint URLs. - MCP endpoint (Streamable HTTP): `https://swapsly.com/mcp` — add it to any MCP client with your API key as the `X-Api-Key` header. ## Auth - Create a per-user API key in the Swapsly app: Settings → API keys. Keys look like `sk_live_...`. - Send it as `X-Api-Key: sk_live_...` (or `Authorization: Bearer sk_live_...`) on every request. - Reading public pages (below) needs no key at all. ## Tools / endpoints - get_me — `GET /v1/me` — who the key acts as, and which scopes it carries. Any valid key may call it. - search_listings — `GET /v1/listings/search?query=...` — search active listings (optional category, condition, price, and geo filters). - get_listing — `GET /v1/listings/{id}` — full details for one listing. - parse_intent — `POST /v1/parse` — turn a natural-language description **and/or photos** into a normalized category + specs, ready to list or request. - upload_images — `POST /v1/uploads` — host photos (base64) and get back URLs for `create_listing`. - delete_upload — `POST /v1/uploads/delete` — remove a hosted photo when a draft is abandoned. - create_listing — `POST /v1/listings` — create a listing (trade, sell, or auction), optionally with `bounties`: the requests you want in exchange. - create_request — `POST /v1/requests` — post a standalone "wanted" request (bounty). - list_trades — `GET /v1/trades` — the caller's trades, oldest activity first, cursor-paged. - get_trade — `GET /v1/trades/{tradeId}` — one trade in full. - make_offer — `POST /v1/offers` — propose a trade; returns a confirm link the user opens in-app. - accept_offer — `POST /v1/offers/{tradeId}/accept` — propose accepting an offer; user confirms in-app. - reject_offer — `POST /v1/offers/{tradeId}/reject` — reject the current offer on a trade. ### Listing an item from photos `upload_images` → `parse_intent` (pass the same photos) → `create_listing` with the returned `image_urls`, the parse's `category_id` and `structured_specs`, and any `bounties`. Pass the category and specs through **verbatim**: they are canonical Swapsly values and request matching is exact, so paraphrased specs produce a listing that quietly matches nothing. ## Readable pages Every public page is also served as Markdown — append `.md` to the path, or send `Accept: text/markdown`. Crawlers additionally get prerendered semantic HTML with JSON-LD, so no JavaScript is required. - `https://swapsly.com/listing/{id}` — one listing (schema.org Product). - `https://swapsly.com/bounty/{id}` — one "wanted" request (schema.org Demand). - `https://swapsly.com/user/{id}` — a public seller profile and their active listings. - `https://swapsly.com/c/{category-slug}` — a browsable category of listings. - `https://swapsly.com/trade/completed/{id}` — an anonymised record of a completed trade. - [Sitemap](https://swapsly.com/sitemap.xml): index of every crawlable page. ## Optional - [llms-full.txt](https://swapsly.com/llms-full.txt): this file plus the entire agent guide and reference tables, in one fetch. - [Privacy policy](https://swapsly.com/privacy) - [Terms of service](https://swapsly.com/terms) - [Prohibited items](https://swapsly.com/prohibited-items): what may not be listed. ## Notes - Rate limits are per API key (default 60 reads/minute, 100 writes/hour); a `429` includes `Retry-After`. - Reference ids: listing_type 1=Trade 2=Auction 3=Sell 4=Sell-or-Trade; condition 1=New…5=Poor; exchange_method 1=Meetup 2=Shipped 3=Meetup-or-Shipped 4=on_ledger (XRPL). --- # Swapsly for AI agents Swapsly is a peer-to-peer marketplace for trading, selling, and auctioning second-hand items. This guide is for building agents (or wiring an MCP client) that act **on behalf of a user**. Three ways to connect: - **REST API** — any framework/language. See [openapi.json](https://swapsly.com/openapi.json). - **MCP server** — Claude Desktop, ChatGPT, or any MCP client, over Streamable HTTP. - **Plain HTTP, no key** — every public page is readable as Markdown or as JSON-LD-annotated HTML. See §7. The REST and MCP surfaces expose the **same tools** and are backed by the same logic, so behavior is identical. --- ## 1. Get an API key In the Swapsly app: **Settings → API keys → Create key**. The key is shown **once** — copy it. It looks like `sk_live_...` and is tied to your user account; anything an agent does with it is attributed to you and respects your permissions. Send it on every request: ``` X-Api-Key: sk_live_xxxxxxxxxxxx ``` (or `Authorization: Bearer sk_live_xxxxxxxxxxxx`). Scopes on a key: `listings:read`, `parse`, `listings:write`, `requests:write`, `offers:read`, `offers:write`. You can restrict a key to a subset when creating it. --- ## 2. REST quickstart Base URL: `https://swapsly.com/v1`. This is a thin proxy in front of the `agent-api` edge function, which stays directly callable at `https://{project-ref}.functions.supabase.co/agent-api/v1` if you prefer to skip the CDN. `GET https://swapsly.com/v1/` needs **no key** and returns the live tool list, scopes, and endpoint URLs — use it to discover the API rather than hardcoding this document. Search: ```bash curl "https://swapsly.com/v1/listings/search?query=pokemon%20charizard&limit=10" \ -H "X-Api-Key: sk_live_xxx" ``` Parse a description into a normalized listing intent, then create the listing: ```bash curl -X POST "https://swapsly.com/v1/parse" -H "X-Api-Key: sk_live_xxx" \ -H "Content-Type: application/json" \ -d '{"intent_type":"list","text":"Charizard base set holo, lightly played"}' # → { category_id, structured_specs, ... } curl -X POST "https://swapsly.com/v1/listings" -H "X-Api-Key: sk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "title":"Charizard Base Set Holo", "listing_type":3, "condition_id":3, "exchange_method_id":3, "category_id":, "structured_specs":, "sale_price":250, "image_urls":["https://.../card.jpg"] }' ``` Post a "wanted" request: ```bash curl -X POST "https://swapsly.com/v1/requests" -H "X-Api-Key: sk_live_xxx" \ -H "Content-Type: application/json" \ -d '{"title":"Looking for Blastoise holo","category_id":}' ``` --- ## 3. MCP quickstart The MCP server speaks JSON-RPC over Streamable HTTP at: ``` https://swapsly.com/mcp ``` (Also directly callable at `https://{project-ref}.functions.supabase.co/agent-mcp`.) Add it to an MCP client with the API key as a header. Example (Claude Code): ```bash claude mcp add --transport http swapsly https://swapsly.com/mcp \ --header "X-Api-Key: sk_live_xxx" ``` Or inspect it directly: ```bash npx @modelcontextprotocol/inspector # connect to the URL above, header X-Api-Key: sk_live_xxx ``` `tools/list` returns the tools below with JSON-Schema inputs identical to the OpenAPI spec. It is the live list — prefer it over this table, which is maintained by hand. --- ## 4. Tools | Tool | Does | Scope | |---|---|---| | `get_me` | Who the key acts as + the scopes it carries | any valid key | | `search_listings` | Search active listings (text + optional category / condition / price / geo filters) | `listings:read` | | `get_listing` | Full details of one listing | `listings:read` | | `parse_intent` | NL text and/or **photos** → normalized category + canonical specs (feed into create_listing / create_request) | `parse` | | `upload_images` | Host photos and get back URLs for `create_listing` | `listings:write` | | `delete_upload` | Remove a hosted photo when a draft is abandoned | `listings:write` | | `create_listing` | Create a listing (trade / sell / auction), optionally with attached requests | `listings:write` | | `create_request` | Post a standalone "wanted" request (bounty) | `requests:write` | | `list_trades` | The caller's trades, oldest activity first, cursor-paged | `offers:read` | | `get_trade` | One trade in full | `offers:read` | | `make_offer` | Propose a trade on a listing — **staged for human confirm** | `offers:write` | | `accept_offer` | Propose accepting an offer — **staged for human confirm** | `offers:write` | | `reject_offer` | Reject the current offer on a trade | `offers:write` | Reference ids: **listing_type** 1=Trade 2=Auction 3=Sell 4=Sell-or-Trade · **condition** 1=New 2=Like New 3=Good 4=Fair 5=Poor · **exchange_method** 1=Meetup 2=Shipped 3=Meetup-or-Shipped. --- ## 4a. Listing an item from photos Three calls: ```bash # 1. Host the photos. Batch them — one call costs one write against the hourly limit. curl -X POST https://swapsly.com/v1/uploads -H "X-Api-Key: sk_live_xxx" \ -H 'content-type: application/json' \ -d '{"images":["data:image/jpeg;base64,/9j/4AAQ..."]}' # → { "data": { "images": [ { "path": "agent_…_0", "url": "https://…/item_images/agent_…_0" } ] } } # 2. Read them — same base64, and Swapsly does the vision. curl -X POST https://swapsly.com/v1/parse -H "X-Api-Key: sk_live_xxx" \ -H 'content-type: application/json' \ -d '{"intent_type":"list","images":["data:image/jpeg;base64,/9j/4AAQ..."]}' # → { "title": "…", "category_id": 412, "structured_specs": { "Brand": "Sony", … } } # 3. Create it, with the hosted URLs and what you want in exchange. curl -X POST https://swapsly.com/v1/listings -H "X-Api-Key: sk_live_xxx" \ -H 'content-type: application/json' \ -d '{"title":"Sony WH-1000XM4","listing_type":1,"condition_id":2, "exchange_method_id":1,"category_id":412,"estimated_value":120, "image_urls":["https://…/item_images/agent_…_0"], "structured_specs":{"Brand":"Sony"}, "bounties":[{"title":"Mechanical keyboard","category_id":388}]}' ``` Four things worth knowing: - **Pass `category_id` and `structured_specs` through verbatim.** They are canonical values from Swapsly's own taxonomy, and request matching is exact containment — rephrasing a spec value produces a listing that is created successfully and then quietly matches nothing. - **A photo cannot tell you the price or the condition.** Ask the person you act for rather than guessing; those are the two fields a human always wants to set themselves. - **`bounties` applies only to Trade (1) and Sell-or-Trade (4).** Sending them with a Sell or Auction listing is rejected rather than silently dropped. - **Keep each upload request under ~3MB of base64.** The edge proxy in front of this API rejects larger bodies before they reach it, so an oversized batch fails with an error this API never sees and cannot explain. Downscale first. --- ## 5. The human-confirm model (important) Agents can **propose** trades but cannot move value on their own. `make_offer` and `accept_offer` **do not execute** — they validate the action and return: ```json { "status": "pending_confirmation", "pending_action_id": "…", "confirm_url": "https://swapsly.com/confirm?a=…", "summary": "Offer 1 of your item(s) for 1 of their item(s).", "expires_at": "…" } ``` Surface the `summary` and `confirm_url` to the user. Nothing happens until they open the link in the Swapsly app and confirm, at which point the trade executes **from their own session**. Staged actions expire after 24 hours. `reject_offer` is not value-moving and executes immediately. ### Platform fees Trades that settle on the XRP Ledger carry a platform fee, split evenly between the two parties. Where one applies, `accept_offer` returns it alongside the staged action: ```json { "status": "pending_confirmation", "platform_fee": { "total_drops": 250000, "your_share_drops": 125000, "side": "buyer" }, "summary": "Accept the current offer on trade …. This trade settles on the XRP Ledger; you will pay 0.125 XRP in platform fees (half of 0.25 XRP, split with the other party)." } ``` `your_share_drops` is what **this user** pays — the counterparty pays the rest, so do not quote `total_drops` as their cost. A `buyer` adds their share to what they pay; a `seller` has it taken from their proceeds. The field is absent on trades that carry no fee, including every physical trade. The quoted amount is frozen when the action is staged, so it is the amount that will actually be charged. Fees apply only to on-ledger trades. `make_offer` cannot create one — its `exchange_method_id` accepts 1, 2 or 3 — so a fee can only ever appear on `accept_offer`. --- ## 6. Rate limits & errors - Per key: **60 reads/minute**, **100 writes/hour** (defaults; tunable server-side). A `429` includes `Retry-After` (seconds). - Errors are JSON: `{ "error": "...", "message": "..." }`. Common codes: `401 unauthorized` (bad/missing/revoked key), `403 forbidden` (missing scope), `400 invalid_input` / `offer_invalid`, `404 not_found`. --- ## 7. Reading Swapsly without an API key If you only need to *read*, you do not need a key or the API at all. Every public page answers to three representations at the same URL: | Want | How | |---|---| | Markdown | Append `.md` — `https://swapsly.com/listing/{id}.md` | | Markdown | Or send `Accept: text/markdown` to the bare URL | | HTML + JSON-LD | Fetch the bare URL with a recognised crawler User-Agent | | JSON | `https://swapsly.com/v1/listings/{id}` (needs a key) | Crawlable page types: `/listing/{id}` (schema.org `Product`), `/bounty/{id}` (`Demand`), `/user/{id}` (`ProfilePage` › `Person`), `/c/{category-slug}` (`CollectionPage`), `/trade/completed/{id}` (anonymised). [`/sitemap.xml`](https://swapsly.com/sitemap.xml) indexes all of them; [`/robots.txt`](https://swapsly.com/robots.txt) states the crawl policy. A normal browser User-Agent gets the React single-page app instead, which requires JavaScript — so identify yourself honestly and you will get the better representation. --- ## 8. Notes for maintainers - The canonical schema for every tool lives in `supabase/functions/_shared/agent-types.ts`. This file, `llms.txt`, and `openapi.json` should be regenerated from it (a small codegen script is the intended follow-up) so the three never drift. - Public directory listing (Claude Connectors Directory) requires OAuth 2.1; the API-key auth here is the interim. See the plan for the upgrade path (enable Supabase `[auth.oauth_server]`).