# 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":<from parse>, "structured_specs":<from parse>,
        "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":<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]`).
