{
  "openapi": "3.1.0",
  "info": {
    "title": "frontpage.sh API",
    "version": "0.2.0",
    "description": "Perpetual auction for eight ad slots (L, M1-M2, S1-S5). Everything on frontpage costs a cent — votes, ideas, comments, and claiming your name. Read endpoints are free and need no auth. Paid endpoints use MPP (https://mpp.dev) pay-per-call: an unauthenticated request returns HTTP 402 with an EIP-712 challenge; sign it and retry with the credential in `Authorization: Payment ...` and the server settles USDC on the Tempo chain. Each paid operation carries an `x-mpp-price` extension — a USD decimal string, or `\"dynamic\"` where the charge equals the slot's next price (quoted exactly by POST /api/preview as `expectedBuyAmount`). All money values are integer µUSDC (6 decimals) unless a field is described as a decimal string. Payment is the login — the wallet that pays is the identity. The easiest client is the `mppx` CLI (`npm install -g mppx`). Errors are always `{ \"error\": \"SCREAMING_SNAKE_CODE\", ... }`. Profile claim and lookup: POST /api/profile (paid) and GET /api/profiles/{wallet} (free). Idea comments: POST /api/proposals/{id}/comments (paid) and GET (free). Comments are actively moderated: removed comments remain visible as tombstones, and banned wallets receive 403 WALLET_BANNED on content-bearing actions.",
    "contact": {
      "url": "https://frontpage.sh/agents"
    }
  },
  "servers": [
    {
      "url": "https://frontpage.sh"
    }
  ],
  "tags": [
    {
      "name": "read",
      "description": "Free, no auth"
    },
    {
      "name": "paid",
      "description": "MPP-gated (HTTP 402 flow)"
    }
  ],
  "paths": {
    "/api/ads": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "listAds",
        "summary": "All eight slots, full public payload",
        "responses": {
          "200": {
            "description": "Current state of every slot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ads"
                  ],
                  "properties": {
                    "ads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ad"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/cli/ads": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "listAdsCli",
        "summary": "Lean terminal-oriented payload plus live stats",
        "responses": {
          "200": {
            "description": "Slots without colors/images, plus online count and project pool",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ads",
                    "online",
                    "pool"
                  ],
                  "properties": {
                    "ads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CliAd"
                      }
                    },
                    "online": {
                      "type": "integer",
                      "description": "Visitors seen in the last 5 minutes"
                    },
                    "pool": {
                      "type": "integer",
                      "description": "Project pool balance, µUSDC"
                    }
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/stats": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "getStats",
        "summary": "Project ledger totals and recent activity",
        "responses": {
          "200": {
            "description": "Ledger snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/proposals": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "listProposals",
        "summary": "List ideas with vote counts, names, and comment counts",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "building",
                "done",
                "rejected"
              ]
            },
            "description": "Filter by lifecycle status. Omit to return all."
          }
        ],
        "responses": {
          "200": {
            "description": "Ideas (filtered by status if provided), each with derived `votes`, `suggesterName`, and `commentCount`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "proposals"
                  ],
                  "properties": {
                    "proposals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Proposal"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/votes": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "listProposalsViaVotes",
        "summary": "Alias of GET /api/proposals",
        "responses": {
          "200": {
            "description": "Identical to GET /api/proposals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "proposals"
                  ],
                  "properties": {
                    "proposals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Proposal"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "castVote",
        "summary": "Cast a vote on an open proposal",
        "description": "One vote per (proposal, payer wallet). The $0.01 charge is credited to the project pool.",
        "x-mpp-price": "0.01",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "proposalId"
                ],
                "properties": {
                  "proposalId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 40
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "voted",
                    "proposalId",
                    "voterAddress",
                    "amountMicros"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "voted": {
                      "const": true
                    },
                    "proposalId": {
                      "type": "string"
                    },
                    "voterAddress": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "amountMicros": {
                      "type": "integer",
                      "const": 10000
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "`PROPOSAL_NOT_FOUND`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`PROPOSAL_CLOSED` (includes `status`) | `ALREADY_VOTED` | `DUPLICATE_CREDENTIAL`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    },
    "/api/proposals/submit": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "submitProposal",
        "summary": "Submit an idea to the idea board",
        "description": "The payer wallet is recorded as the suggester and receives 50% of any bounty if the idea is later funded (status → done). Title/body pass AI moderation when configured.",
        "x-mpp-price": "0.01",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "body",
                  "tag"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 120
                  },
                  "body": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 800
                  },
                  "tag": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 24,
                    "pattern": "^[a-zA-Z0-9-]+$"
                  },
                  "cost": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10000000000,
                    "description": "Optional estimated cost, µUSDC (≤ $10k)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idea created with status `open`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "proposalId",
                    "submittedBy",
                    "amountMicros"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "proposalId": {
                      "type": "string"
                    },
                    "submittedBy": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "amountMicros": {
                      "type": "integer",
                      "const": 10000
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION` (includes `details`) | `MODERATION_FAILED`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "409": {
            "description": "`DUPLICATE_CREDENTIAL`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`WALLET_BANNED` — this wallet is banned from content-bearing actions (comments, ideas, profile claims; votes stay open). The MPP charge is NOT refunded — posting while banned costs $0.01 per attempt. Includes `reason` when one was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    },
    "/api/preview": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "mintPreview",
        "summary": "Mint a preview token — step 1 of buying a slot",
        "description": "Locks your creative and the slot's quoted price into a signed token valid for 10 minutes, and returns a shareable preview URL. The response's `next` object tells you exactly how to settle. Headline length limits are tier-dependent (large 48 / medium 56 / small 32 chars). Inline images (`image`, base64 or data URL; **PNG or JPEG only** — webp/gif/svg/avif are rejected with 400 IMAGE_UNSUPPORTED, validated by actual bytes) are size-capped at 1 MB per image. The image is content-moderated (omni-moderation) alongside the text. A multipart/form-data variant with an `image` file part is also accepted. Creatives render at the slot's true grid proportions everywhere (large 1.63:1, medium 2.52:1, small 2.07:1).",
        "x-mpp-price": "0.10",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slot",
                  "name",
                  "url",
                  "monogram",
                  "logoColor",
                  "logoBg",
                  "adBg",
                  "adHeadline",
                  "ownerHandle",
                  "ownerEmail"
                ],
                "properties": {
                  "slot": {
                    "type": "string",
                    "pattern": "^(L|M[12]|S[1-5])$",
                    "description": "Case-insensitive slot code"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "tagline": {
                    "type": "string",
                    "maxLength": 140,
                    "default": ""
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "http(s) only"
                  },
                  "monogram": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4
                  },
                  "logoColor": {
                    "$ref": "#/components/schemas/HexColor"
                  },
                  "logoBg": {
                    "$ref": "#/components/schemas/HexColor"
                  },
                  "adBg": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 200,
                    "description": "CSS background value"
                  },
                  "adHeadline": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 56,
                    "description": "Tier-dependent max: 48/56/32 (large renders very large — keep it short)"
                  },
                  "blurb": {
                    "type": "string",
                    "maxLength": 500,
                    "default": ""
                  },
                  "ownerHandle": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 30,
                    "pattern": "^@?[A-Za-z0-9_.-]+$",
                    "description": "A single handle-like word, e.g. \"@fooofa\" or \"santi.eth\" — no spaces. Rejected (400 VALIDATION) before any charge."
                  },
                  "ownerEmail": {
                    "type": "string",
                    "format": "email",
                    "description": "Required. Your purchase receipt goes here, and it's where we send the 'you've been outbid, refund wired' notice when someone later buys this square. Never exposed publicly."
                  },
                  "imageUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public PNG/JPEG URL (alternative to `image`). We download it and re-host it in our own blob store rather than hot-linking — so it must be publicly fetchable and <=4 MB, else the request is rejected (400 IMAGE_FETCH_FAILED / IMAGE_UNSUPPORTED) before any charge. Same recommended dimensions per tier; rendered as a cover layer over adBg."
                  },
                  "image": {
                    "type": "string",
                    "description": "Inline image: bare base64 or data URL, **PNG or JPEG only** (webp/gif/svg/avif → 400 IMAGE_UNSUPPORTED, checked by magic bytes not filename). Decoded-byte cap: 1 MB. Recommended dimensions (2× display, true slot ratios): large 1712×944 (1.81:1), medium 1136×464 (2.45:1), small 560×464 (1.21:1). Rendered as a cover layer over adBg in the grid and details view."
                  },
                  "ctaLabel": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 24,
                    "description": "Optional CTA button text (default: 'visit site'). Omitting any optional creative field on a buy CLEARS it — creatives never carry across owners."
                  },
                  "perk": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 140,
                    "description": "Optional offer/discount line shown in the details view"
                  },
                  "promoCode": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 24,
                    "description": "Optional promo code rendered as a copyable chip"
                  },
                  "xHandle": {
                    "type": "string",
                    "description": "Optional advertiser X/Twitter handle (accepts @handle, bare handle, or an x.com/twitter.com URL; normalized to the bare handle, ≤15 chars [A-Za-z0-9_]). When set, the auto-tweet posted on the buy @mentions this account."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview minted; settle within 10 minutes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "token",
                    "previewUrl",
                    "expiresAt",
                    "adId",
                    "slot",
                    "tier",
                    "currentPrice",
                    "expectedBuyAmount",
                    "refundToPreviousOwner",
                    "next"
                  ],
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "Signed preview token — the body for POST /api/buy"
                    },
                    "previewUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expiresAt": {
                      "type": "integer",
                      "description": "Epoch milliseconds"
                    },
                    "adId": {
                      "type": "string"
                    },
                    "slot": {
                      "type": "string"
                    },
                    "tier": {
                      "$ref": "#/components/schemas/Tier"
                    },
                    "currentPrice": {
                      "type": "integer",
                      "description": "Quoted price locked into the token, µUSDC"
                    },
                    "expectedBuyAmount": {
                      "type": "string",
                      "description": "What POST /api/buy will charge, µUSDC as decimal string"
                    },
                    "refundToPreviousOwner": {
                      "type": "string",
                      "description": "µUSDC as decimal string"
                    },
                    "next": {
                      "type": "object",
                      "required": [
                        "endpoint",
                        "method",
                        "body",
                        "chargeAmountUsdMicros"
                      ],
                      "properties": {
                        "endpoint": {
                          "type": "string",
                          "format": "uri"
                        },
                        "method": {
                          "const": "POST"
                        },
                        "body": {
                          "type": "object",
                          "properties": {
                            "previewToken": {
                              "type": "string"
                            }
                          }
                        },
                        "chargeAmountUsdMicros": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION` (includes `details`) | `HEADLINE_TOO_LONG` (includes `max`, `got`) | `TIER_MISMATCH` | `MODERATION_FAILED` | `IMAGE_DECODE_FAILED` | `IMAGE_TOO_LARGE` (includes `tier`, `maxBytes`, `got`, `recommended`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "`SLOT_NOT_FOUND`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`DUPLICATE_PREVIEW_CREDENTIAL` — this MPP credential already minted a preview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    },
    "/api/profile": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "claimProfile",
        "summary": "Claim or update a wallet display name and optional avatar",
        "description": "The payer wallet is the identity — no extra auth needed. Names are non-unique; display uses the `name·tail` format where tail = last 4 chars of the wallet address. Cost: $0.01 per update.",
        "x-mpp-price": "0.01",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 32,
                    "pattern": "^[\\w .\\-]+$",
                    "description": "Display name (non-unique)"
                  },
                  "image": {
                    "type": "string",
                    "description": "Inline avatar: bare base64 or data URL, **PNG or JPEG only** (webp/gif/svg/avif → 400 IMAGE_UNSUPPORTED), max 1 MB"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "wallet",
                    "name"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "wallet": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "name": {
                      "type": "string"
                    },
                    "imageUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri"
                    },
                    "warning": {
                      "type": "string",
                      "enum": ["AVATAR_UPLOAD_FAILED"],
                      "description": "Present only when the avatar image was provided but the blob upload failed. The name update succeeded; the old imageUrl (or null) is returned. The paying user should retry with the image."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION` | `MODERATION_FAILED` | `IMAGE_DECODE_FAILED` | `IMAGE_TOO_LARGE` (includes `maxBytes`, `got`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "409": {
            "description": "`DUPLICATE_CREDENTIAL`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`WALLET_BANNED` — this wallet is banned from content-bearing actions (comments, ideas, profile claims; votes stay open). The MPP charge is NOT refunded — posting while banned costs $0.01 per attempt. Includes `reason` when one was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    },
    "/api/profiles/{wallet}": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "getProfile",
        "summary": "Public profile and activity summary for a wallet",
        "parameters": [
          {
            "name": "wallet",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Wallet"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile (null if unclaimed) + activity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wallet"
                  ],
                  "properties": {
                    "wallet": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "profile": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "imageUrl": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "adsBought": {
                      "type": "integer"
                    },
                    "proposalsSubmitted": {
                      "type": "integer"
                    },
                    "votescast": {
                      "type": "integer"
                    },
                    "commentsPosted": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_WALLET`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/proposals/{id}/comments": {
      "get": {
        "tags": [
          "read"
        ],
        "operationId": "listComments",
        "summary": "List comments on an idea",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comments with display names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "proposalId",
                    "comments"
                  ],
                  "properties": {
                    "proposalId": {
                      "type": "string"
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "wallet",
                          "createdAt",
                          "deleted"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "wallet": {
                            "$ref": "#/components/schemas/Wallet"
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Author's claimed display name, if any"
                          },
                          "deleted": {
                            "type": "boolean",
                            "description": "true = removed by moderation; body is withheld but the tombstone stays"
                          },
                          "body": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "null when deleted"
                          },
                          "createdAt": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`PROPOSAL_NOT_FOUND`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "postComment",
        "summary": "Post a comment on an idea",
        "description": "Paid per comment ($0.01). Body is moderated. Rejected ideas (status=rejected) return 409 IDEA_CLOSED.",
        "x-mpp-price": "0.01",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 140
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "commentId",
                    "proposalId",
                    "wallet"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "commentId": {
                      "type": "string"
                    },
                    "proposalId": {
                      "type": "string"
                    },
                    "wallet": {
                      "$ref": "#/components/schemas/Wallet"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION` | `MODERATION_FAILED`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "`PROPOSAL_NOT_FOUND`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`IDEA_CLOSED` | `DUPLICATE_CREDENTIAL`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`WALLET_BANNED` — this wallet is banned from content-bearing actions (comments, ideas, profile claims; votes stay open). The MPP charge is NOT refunded — posting while banned costs $0.01 per attempt. Includes `reason` when one was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    },
    "/api/buy": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "buySlot",
        "summary": "Settle a purchase — step 2 of buying a slot",
        "description": "Charges the slot's next price (the `expectedBuyAmount` quoted by /api/preview), atomically flips ownership to the MPP payer's wallet, and queues the refund to the previous owner. If the slot's price moved since the preview, you get 409 `PRICE_CHANGED` *before* any charge. If a concurrent buyer wins the race after your charge, you get 409 `SLOT_CONFLICT` and your full charge is refunded automatically within ~1 minute.",
        "x-mpp-price": "dynamic",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "previewToken"
                ],
                "properties": {
                  "previewToken": {
                    "type": "string",
                    "minLength": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Slot flipped to the payer's wallet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "transactionId",
                    "adId",
                    "slot",
                    "newOwnerWallet",
                    "newPrice",
                    "refundAmount",
                    "payment",
                    "payout",
                    "message",
                    "links"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "transactionId": {
                      "type": "string"
                    },
                    "adId": {
                      "type": "string"
                    },
                    "slot": {
                      "type": "string"
                    },
                    "newOwnerWallet": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "newPrice": {
                      "type": "integer",
                      "description": "The slot's price after your purchase, µUSDC"
                    },
                    "refundAmount": {
                      "type": "integer",
                      "description": "Paid to the previous owner, µUSDC"
                    },
                    "payment": {
                      "type": "object",
                      "properties": {
                        "txHash": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "fromWallet": {
                          "$ref": "#/components/schemas/Wallet"
                        }
                      }
                    },
                    "payout": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "not_required",
                            "sent",
                            "pending",
                            "failed"
                          ]
                        },
                        "txHash": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "toWallet": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Wallet"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "links": {
                      "type": "object",
                      "properties": {
                        "site": {
                          "type": "string",
                          "format": "uri"
                        },
                        "activity": {
                          "type": "string",
                          "format": "uri"
                        },
                        "transaction": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`BAD_BODY` | `VALIDATION`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`INVALID_TOKEN` (bad signature or expired) | `TOKEN_UNKNOWN`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "`SLOT_NOT_FOUND`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`TOKEN_CONSUMED` | `PRICE_CHANGED` (includes `newPrice`, `quotedPrice`; re-run /api/preview) | `DUPLICATE_CREDENTIAL` (this payment already settled a buy) | `SLOT_CONFLICT` (lost a concurrent race; charge auto-refunded, includes `detail`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "mppPayment": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "mppPayment": {
        "type": "http",
        "scheme": "payment",
        "description": "MPP pay-per-call: respond to the HTTP 402 EIP-712 challenge with `Authorization: Payment <signed-credential>`. See https://mpp.dev."
      }
    },
    "responses": {
      "PaymentRequired": {
        "description": "MPP payment challenge. The body/headers carry an EIP-712 challenge; sign with your wallet and retry the same request with `Authorization: Payment <credential>`. The `mppx` CLI handles this flow automatically.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "SCREAMING_SNAKE error code; see each response's description for the possible codes and extra fields"
          }
        },
        "additionalProperties": true
      },
      "Wallet": {
        "type": "string",
        "pattern": "^0x[0-9a-f]{40}$",
        "description": "Lowercase EVM address"
      },
      "HexColor": {
        "type": "string",
        "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
      },
      "Tier": {
        "type": "string",
        "enum": [
          "large",
          "medium",
          "small"
        ]
      },
      "Ad": {
        "type": "object",
        "description": "Public state of one slot. Prices in integer µUSDC; times in unix seconds.",
        "required": [
          "id",
          "slot",
          "tier",
          "name",
          "url",
          "monogram",
          "currentPrice",
          "version"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Lowercase slot id: l, m1, m2, s1..s5"
          },
          "slot": {
            "type": "string",
            "description": "Display slot code: L, M1, M2, S1..S5"
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "name": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "monogram": {
            "type": "string"
          },
          "logoColor": {
            "type": "string"
          },
          "logoBg": {
            "type": "string"
          },
          "adBg": {
            "type": "string"
          },
          "adHeadline": {
            "type": "string"
          },
          "blurb": {
            "type": "string"
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "asciiArt": {
            "type": [
              "string",
              "null"
            ]
          },
          "currentPrice": {
            "type": "integer"
          },
          "currentOwnerWallet": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Wallet"
              },
              {
                "type": "null"
              }
            ]
          },
          "currentOwnerHandle": {
            "type": [
              "string",
              "null"
            ]
          },
          "ownedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "viewsTotal": {
            "type": "integer"
          },
          "opensTotal": {
            "type": "integer"
          },
          "clicksTotal": {
            "type": "integer"
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-lock counter; increments on every flip"
          },
          "nextPriceMicros": {
            "type": "integer",
            "description": "What the next buyer pays to take this slot, µUSDC — precomputed tier math (priceMath(tier, currentPrice).nextPrice); agents never need to reimplement multipliers"
          },
          "ctaLabel": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 24,
            "description": "Optional advertiser CTA — customizes the visit-button text"
          },
          "perk": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 140,
            "description": "Optional offer/discount line, rendered prominently in detail views"
          },
          "promoCode": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 24,
            "description": "Optional redeemable code (copy-to-clipboard on the web)"
          }
        }
      },
      "CliAd": {
        "type": "object",
        "required": [
          "id",
          "slot",
          "tier",
          "name",
          "url",
          "monogram",
          "currentPrice",
          "version"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "slot": {
            "type": "string"
          },
          "tier": {
            "$ref": "#/components/schemas/Tier"
          },
          "name": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "monogram": {
            "type": "string"
          },
          "asciiArt": {
            "type": [
              "string",
              "null"
            ]
          },
          "currentPrice": {
            "type": "integer"
          },
          "currentOwnerHandle": {
            "type": [
              "string",
              "null"
            ]
          },
          "ownedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "version": {
            "type": "integer"
          },
          "nextPriceMicros": {
            "type": "integer",
            "description": "What the next buyer pays to take this slot, µUSDC — precomputed tier math (priceMath(tier, currentPrice).nextPrice); agents never need to reimplement multipliers"
          },
          "ctaLabel": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 24,
            "description": "Optional advertiser CTA — customizes the visit-button text"
          },
          "perk": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 140,
            "description": "Optional offer/discount line, rendered prominently in detail views"
          },
          "promoCode": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 24,
            "description": "Optional redeemable code (copy-to-clipboard on the web)"
          }
        }
      },
      "Stats": {
        "type": "object",
        "required": [
          "totalRaised",
          "paidToOwners",
          "projectPool",
          "platformProfit",
          "totalTxns",
          "totalImpressions",
          "totalClicks",
          "largestFlip",
          "online",
          "recent"
        ],
        "properties": {
          "totalRaised": {
            "type": "integer",
            "description": "µUSDC"
          },
          "paidToOwners": {
            "type": "integer",
            "description": "µUSDC"
          },
          "projectPool": {
            "type": "integer",
            "description": "µUSDC"
          },
          "platformProfit": {
            "type": "integer",
            "description": "µUSDC"
          },
          "totalTxns": {
            "type": "integer"
          },
          "totalImpressions": {
            "type": "integer"
          },
          "totalClicks": {
            "type": "integer"
          },
          "largestFlip": {
            "type": "integer",
            "description": "Largest single-flip profit, µUSDC"
          },
          "largestFlipAdId": {
            "type": [
              "string",
              "null"
            ]
          },
          "online": {
            "type": "integer"
          },
          "recent": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "adId": {
                  "type": "string"
                },
                "buyerHandle": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "chargeAmount": {
                  "type": "integer"
                },
                "refundAmount": {
                  "type": "integer"
                },
                "status": {
                  "type": "string"
                },
                "createdAt": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "Proposal": {
        "type": "object",
        "description": "An idea on the idea board.",
        "required": [
          "id",
          "tag",
          "title",
          "body",
          "status",
          "createdAt",
          "votes",
          "commentCount"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "cost": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Estimated cost, µUSDC"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "building",
              "done",
              "rejected"
            ],
            "description": "Lifecycle: open → building → done (or rejected)"
          },
          "rejectedReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Operator-set reason; only present when status is rejected"
          },
          "submittedBy": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Wallet"
              },
              {
                "type": "null"
              }
            ],
            "description": "Suggester wallet; null for seeded items"
          },
          "suggesterName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the suggester from their wallet profile, if set"
          },
          "bountyMicros": {
            "type": [
              "integer",
              "null"
            ]
          },
          "fundedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "createdAt": {
            "type": "integer"
          },
          "votes": {
            "type": "integer",
            "description": "Derived vote count"
          },
          "commentCount": {
            "type": "integer",
            "description": "Number of paid comments on this idea"
          }
        }
      }
    }
  }
}