{
  "openapi": "3.1.0",
  "info": {
    "title": "Swapsly Agent API",
    "version": "1.0.0",
    "description": "Programmatic access to the Swapsly peer-to-peer marketplace for AI agents acting on behalf of a user. Search and browse listings, parse natural language into listings/requests, create listings and 'wanted' requests, and propose trades. Value-moving actions (offers, accepting trades) are staged and require the user to confirm in the Swapsly app \u2014 they never execute on the API key alone. The canonical schema source is agent-types.ts in the Supabase functions."
  },
  "servers": [
    {
      "url": "https://swapsly.com/v1",
      "description": "Public base (Vercel-fronted). Also directly callable at https://{project-ref}.functions.supabase.co/agent-api/v1"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Per-user API key issued in the Swapsly app (Settings \u2192 API keys). Format: sk_live_... . May also be sent as `Authorization: Bearer sk_live_...`."
      }
    },
    "schemas": {
      "Specs": {
        "type": "object",
        "additionalProperties": {
          "type": [
            "string",
            "number"
          ]
        },
        "description": "attribute_name \u2192 canonical value, as produced by parse_intent."
      },
      "ParseIntentRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "intent_type"
        ],
        "properties": {
          "intent_type": {
            "type": "string",
            "enum": [
              "list",
              "request"
            ]
          },
          "text": {
            "type": "string",
            "description": "Natural-language item description"
          },
          "category_id": {
            "type": "integer"
          },
          "structured_specs": {
            "$ref": "#/components/schemas/Specs"
          },
          "asset_class": {
            "type": "string",
            "enum": [
              "physical",
              "digital"
            ]
          },
          "images": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "string"
            },
            "description": "Photos of the item as base64 data URIs. The first 4 are read. Provide text, images, or category_id."
          }
        },
        "description": "Provide at least one of text, images, or category_id."
      },
      "CreateListingRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "title",
          "listing_type",
          "condition_id",
          "exchange_method_id",
          "category_id"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "listing_type": {
            "type": "integer",
            "enum": [
              1,
              2,
              3,
              4
            ],
            "description": "1=Trade 2=Auction 3=Sell 4=Sell or Trade"
          },
          "condition_id": {
            "type": "integer",
            "enum": [
              1,
              2,
              3,
              4,
              5
            ],
            "description": "1=New 2=Like New 3=Good 4=Fair 5=Poor"
          },
          "exchange_method_id": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ],
            "description": "1=Meetup 2=Shipped 3=Meetup or Shipped"
          },
          "category_id": {
            "type": "integer"
          },
          "structured_specs": {
            "$ref": "#/components/schemas/Specs"
          },
          "image_urls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Already-hosted image URLs \u2014 get them from POST /uploads.",
            "maxItems": 10
          },
          "estimated_value": {
            "type": "number",
            "description": "Required (>0) when listing_type=1 (Trade)"
          },
          "sale_price": {
            "type": "number",
            "description": "Required (>0) when listing_type=3 or 4 (Sell / Sell or Trade)"
          },
          "auction_starting_bid": {
            "type": "number",
            "description": "Required (>0) when listing_type=2 (Auction)"
          },
          "auction_reserve_price": {
            "type": "number"
          },
          "auction_buy_now_price": {
            "type": "number"
          },
          "auction_duration_id": {
            "type": "integer"
          },
          "bounties": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Bounty"
            },
            "description": "Requests attached to this listing. Only valid for listing_type 1 (Trade) and 4 (Sell or Trade) \u2014 sending them with 2 or 3 is rejected."
          }
        }
      },
      "CreateRequestRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Provide at least one of title, description, or category_id.",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "category_id": {
            "type": "integer"
          },
          "structured_specs": {
            "$ref": "#/components/schemas/Specs"
          }
        }
      },
      "MakeOfferRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "target_listing_id",
          "other_user_id",
          "exchange_method_id"
        ],
        "properties": {
          "target_listing_id": {
            "type": "string",
            "format": "uuid"
          },
          "other_user_id": {
            "type": "string",
            "format": "uuid",
            "description": "The listing owner (counterparty)"
          },
          "exchange_method_id": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ]
          },
          "offered_item_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The caller's own item ids to offer"
          },
          "requested_item_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The counterparty's item ids requested"
          },
          "offered_cash": {
            "type": "number",
            "minimum": 0
          },
          "requested_cash": {
            "type": "number",
            "minimum": 0
          },
          "message": {
            "type": "string"
          }
        }
      },
      "PendingConfirmation": {
        "type": "object",
        "description": "A value-moving action that has been validated and staged for the user to confirm in-app.",
        "properties": {
          "status": {
            "type": "string",
            "example": "pending_confirmation"
          },
          "pending_action_id": {
            "type": "string",
            "format": "uuid"
          },
          "confirm_url": {
            "type": "string",
            "description": "The user opens this in the Swapsly app to confirm."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "summary": {
            "type": "string"
          },
          "platform_fee": {
            "$ref": "#/components/schemas/PlatformFee"
          }
        }
      },
      "PlatformFee": {
        "type": "object",
        "description": "Platform fee on a trade that settles on the XRP Ledger. Split evenly between the two parties, so your_share_drops \u2014 not total_drops \u2014 is what this user pays. Frozen when the action is staged, so it is the amount that will actually be charged. Absent on trades carrying no fee, including every physical trade. Only accept_offer can carry one: make_offer cannot create an on-ledger trade.",
        "properties": {
          "total_drops": {
            "type": "integer",
            "description": "Whole fee in drops, across both parties."
          },
          "your_share_drops": {
            "type": "integer",
            "description": "What this user pays \u2014 half of total_drops."
          },
          "side": {
            "type": "string",
            "enum": [
              "buyer",
              "seller"
            ],
            "description": "A buyer adds their share to what they pay; a seller has it taken from their proceeds."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "retry_after": {
            "type": "integer"
          }
        }
      },
      "Identity": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "handle": {
            "type": "string",
            "nullable": true
          },
          "display_name": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Bounty": {
        "type": "object",
        "additionalProperties": false,
        "description": "A request attached to a listing \u2014 what the owner wants in exchange. Provide at least one of title, description, or category_id.",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "category_id": {
            "type": "integer"
          },
          "structured_specs": {
            "$ref": "#/components/schemas/Specs"
          }
        }
      },
      "UploadImagesRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "images"
        ],
        "properties": {
          "images": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "type": "string"
            },
            "description": "Base64 data URIs (data:image/jpeg;base64,...). Bare base64 is accepted only alongside content_type. JPEG, PNG, WebP and HEIC only \u2014 the bytes are checked, not the label. Keep a request under ~3MB total: the public edge proxy rejects larger bodies before they reach this API."
          },
          "content_type": {
            "type": "string",
            "description": "Fallback MIME type for bare-base64 entries."
          }
        }
      },
      "UploadedImage": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Storage key \u2014 pass to /uploads/delete."
          },
          "url": {
            "type": "string",
            "description": "Public URL \u2014 pass to create_listing as image_urls."
          },
          "content_type": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          }
        }
      },
      "DeleteUploadRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "path"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "The `path` from an upload result (not the URL)."
          }
        }
      },
      "TradeParty": {
        "type": "object",
        "properties": {
          "listing_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "estimated_value": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Trade": {
        "type": "object",
        "properties": {
          "trade_id": {
            "type": "string",
            "format": "uuid"
          },
          "state": {
            "type": "string",
            "enum": [
              "offered",
              "countered",
              "accepted",
              "rejected",
              "cancelled",
              "expired",
              "completed"
            ],
            "description": "Derived: settled outcomes come from the trade, offered/countered from the current offer, expired from trade_expiry."
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ],
            "description": "Whose the CURRENT offer is."
          },
          "actionable": {
            "type": "boolean",
            "description": "True only when the caller can still accept or reject."
          },
          "counterparty": {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true
              },
              "handle": {
                "type": "string",
                "nullable": true
              },
              "display_name": {
                "type": "string",
                "nullable": true
              },
              "url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "target_listing": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TradeParty"
              }
            ],
            "nullable": true,
            "description": "Derived, not stored: recovered from the bounty application when there is one, else the most valuable requested item."
          },
          "offered_items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TradeParty"
            }
          },
          "requested_items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TradeParty"
            }
          },
          "offered_cash": {
            "type": "string",
            "nullable": true,
            "description": "Null rather than \"0\" when there is none."
          },
          "requested_cash": {
            "type": "string",
            "nullable": true
          },
          "exchange_method_id": {
            "type": "integer",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "Written by the counterparty \u2014 untrusted text."
          },
          "url": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TradePage": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Trade"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Opaque. Pass back verbatim as `cursor`. Null when there is no further page."
          },
          "has_more": {
            "type": "boolean"
          }
        }
      }
    }
  },
  "paths": {
    "/me": {
      "get": {
        "operationId": "get_me",
        "summary": "Who this key acts as",
        "description": "Returns the key owner's identity and the scopes the key carries. Any valid key may call it \u2014 a narrowed key still needs to be able to report what it was narrowed to.",
        "responses": {
          "200": {
            "description": "Identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/listings/search": {
      "get": {
        "operationId": "search_listings",
        "summary": "Search active listings",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "longitude",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "latitude",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "category_path",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Category.path prefix, e.g. /1/"
          },
          {
            "name": "condition_ids",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "CSV of condition ids, e.g. 1,2"
          },
          {
            "name": "listing_types",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "CSV of listing type ids"
          },
          {
            "name": "price_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "price_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "relevance",
                "newest",
                "price_asc",
                "price_desc",
                "distance"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching listings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "results": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/listings/{listing_id}": {
      "get": {
        "operationId": "get_listing",
        "summary": "Get a listing by id",
        "parameters": [
          {
            "name": "listing_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/parse": {
      "post": {
        "operationId": "parse_intent",
        "summary": "Parse NL/category into a normalized Intent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParseIntentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Normalized Intent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/listings": {
      "post": {
        "operationId": "create_listing",
        "summary": "Create a listing",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateListingRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created listing details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/requests": {
      "post": {
        "operationId": "create_request",
        "summary": "Post a 'wanted' request (bounty)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "bounty_id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/offers": {
      "post": {
        "operationId": "make_offer",
        "summary": "Propose a trade/offer (staged for human confirmation)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MakeOfferRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Staged for confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PendingConfirmation"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/offers/{trade_id}/accept": {
      "post": {
        "operationId": "accept_offer",
        "summary": "Propose accepting an offer (staged for human confirmation)",
        "parameters": [
          {
            "name": "trade_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Staged for confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PendingConfirmation"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/offers/{trade_id}/reject": {
      "post": {
        "operationId": "reject_offer",
        "summary": "Reject the current offer on a trade",
        "parameters": [
          {
            "name": "trade_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "operationId": "upload_images",
        "summary": "Host photos for a listing",
        "description": "Uploads images as the key's owner and returns public URLs to pass to create_listing. All-or-nothing: a partial batch is cleaned up.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadImagesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted images",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "images": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/UploadedImage"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or non-image input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads/delete": {
      "post": {
        "operationId": "delete_upload",
        "summary": "Delete a hosted photo",
        "description": "Removes an image previously returned by upload_images. POST rather than DELETE so the CORS method set stays unchanged.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteUploadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "boolean"
                        },
                        "path": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such image, or not deletable by this key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/trades": {
      "get": {
        "operationId": "list_trades",
        "summary": "List the caller's trades",
        "description": "Ordered by activity, OLDEST first, so an interrupted poller can resume from where it stopped without skipping rows.",
        "parameters": [
          {
            "name": "updated_since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "RFC-3339. Inclusive \u2014 a boundary row is replayed rather than dropped."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque next_cursor from a previous response."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "incoming",
                "outgoing"
              ],
              "default": "all"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of trades",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TradePage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Key lacks offers:read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/trades/{trade_id}": {
      "get": {
        "operationId": "get_trade",
        "summary": "Get one trade",
        "parameters": [
          {
            "name": "trade_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Trade",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Trade"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such trade, or the caller is not party to it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
