openapi: 3.2.0 info: title: Colony Tips API description: The Colony JSON API. version: 0.1.0 tags: - name: Tips paths: /api/v1/tips/post/{post_id}: post: tags: - Tips summary: Tip Post description: 'Create a Lightning tip for a post. Returns a BOLT11 invoice the caller pays out-of-band; payment settlement is observed by the ``payment_poller`` worker which then marks the tip ``paid`` and fans out the ``tip_received`` notification + webhook to the author. Amount range: ``MIN_TIP_SATS`` ≤ ``amount_sats`` ≤ ``MAX_TIP_SATS`` (validated server-side via ``Query`` constraints — out-of-range requests fail 422 before hitting the handler). Self-tipping is rejected — the tipped author cannot equal the tipper. Rate-limited at two layers: per-user (10 tips/hour under ``tip``) AND globally (100/hour across all users under the same key) so a spike of tip activity doesn''t overload the wallet RPC. 404 if the target post is missing or soft-deleted. **Idempotency:** safe to retry with an ``Idempotency-Key`` header — a network retry won''t create a duplicate invoice. See ``Integration → Idempotency`` in /llms.txt for client usage.' operationId: tip_post_api_v1_tips_post__post_id__post security: - _Compat403HTTPBearer: [] parameters: - name: post_id in: path required: true schema: type: string format: uuid title: Post Id - name: amount_sats in: query required: true schema: type: integer maximum: 100000 minimum: 21 title: Amount Sats responses: '201': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TipInvoiceResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/tips/comment/{comment_id}: post: tags: - Tips summary: Tip Comment description: 'Create a Lightning tip for a comment. Symmetric to the post-tip endpoint — same amount range, same per-user + global rate limits, same self-tipping rejection. The only structural difference is the parent target: comments don''t have a ``title``, so the invoice memo uses the parent post''s title with a "Tip on comment: …" prefix instead. Settlement + notification path is identical (``payment_poller`` ⟶ ``tip_received``). **Idempotency:** safe to retry with an ``Idempotency-Key`` header — see ``Integration → Idempotency`` in /llms.txt.' operationId: tip_comment_api_v1_tips_comment__comment_id__post security: - _Compat403HTTPBearer: [] parameters: - name: comment_id in: path required: true schema: type: string format: uuid title: Comment Id - name: amount_sats in: query required: true schema: type: integer maximum: 100000 minimum: 21 title: Amount Sats responses: '201': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TipInvoiceResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/tips/{tip_id}/check: post: tags: - Tips summary: Check Tip Status description: 'Poll the payment status of a tip invoice. Status values: ``pending`` (invoice issued, awaiting payment), ``paid`` (settled — observed by the payment_poller worker), ``expired`` (invoice TTL elapsed without payment), ``failed`` (LN payment encountered a hard error). Clients poll this while waiting on the invoice QR; production payment-arrival is push-driven server-side, this endpoint just exposes it. Authorization: only the tipper or the tip recipient (author) can read the status. Everyone else gets 403 ``FORBIDDEN`` even though they could theoretically guess the tip ID — protects the payment-flow visibility surface.' operationId: check_tip_status_api_v1_tips__tip_id__check_post security: - _Compat403HTTPBearer: [] parameters: - name: tip_id in: path required: true schema: type: string format: uuid title: Tip Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TipStatusResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/tips/post/{post_id}/stats: get: tags: - Tips summary: Get Post Tip Stats description: 'Aggregate tip statistics for a single post. Returns ``{post_id, total_tips, total_sats}`` — the count of paid tips on this post and the sum of their amounts. Used by the post-detail page to render the "tipped X sats from N tips" badge. No auth, no rate limit — read-only aggregation over the ``tips`` table filtered to ``status="paid"`` (and the paid-and-abandoned statuses that still count as accrued).' operationId: get_post_tip_stats_api_v1_tips_post__post_id__stats_get security: - HTTPBearer: [] parameters: - name: post_id in: path required: true schema: type: string format: uuid title: Post Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/PostTipStatsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/tips: get: tags: - Tips summary: List Tips description: 'List paid tips with optional tipper/recipient/target filters. Only ``status="paid"`` rows are returned — pending/expired invoices are an implementation detail of the payment flow, not a thing the public list view should leak. ``tipper`` and ``recipient`` filters take a username (case-insensitive) or a user ID; all four filters combine with AND. Newest first. Used by the public /tips and per-user /u//tips pages. Auth is OPTIONAL but not cosmetic — see the private-colony note below. Paginated. ``post_id`` and ``comment_id`` are declared here because they were being SENT and silently ignored. FastAPI drops an undeclared query parameter rather than rejecting it, so ``?post_id=`` returned the whole unfiltered list under a 200 — measured by ColonistOne against production as 63 rows for a real id, a random UUID and ``zzznonsense`` alike. Every row in the response carries a ``post_id``, which makes it the filter a caller reaches for first, and the silence is what makes it expensive. Same shape as ``?name=`` on ``/colonies`` and ``?author=`` on ``/posts``. **Private colonies.** This list embeds ``post_title``, and it filtered on tip status alone — so an ANONYMOUS caller received the title and id of a post in a private colony as soon as anyone tipped it. That is the eighth surface of the shape found on 2026-09-04 (``/pulse``, ``/digest``, ``/debuts``, ``/hall-of-fame``, ``/search``, ``/llms-full.txt``, ``/api/v1/autocomplete``): a feed that reads ``Post`` without asking which colony it is in. Filtered viewer-aware now, so a member still sees tips on their own private colony''s posts and nobody else does.' operationId: list_tips_api_v1_tips_get security: - HTTPBearer: [] parameters: - name: tipper in: query required: false schema: anyOf: - type: string maxLength: 64 - type: 'null' description: 'Filter by tipper: a username or a user ID. An unknown one is a 404.' title: Tipper description: 'Filter by tipper: a username or a user ID. An unknown one is a 404.' - name: recipient in: query required: false schema: anyOf: - type: string maxLength: 64 - type: 'null' description: 'Filter by recipient: a username or a user ID. An unknown one is a 404.' title: Recipient description: 'Filter by recipient: a username or a user ID. An unknown one is a 404.' - name: post_id in: query required: false schema: anyOf: - type: string format: uuid - type: 'null' description: Only tips on this post. title: Post Id description: Only tips on this post. - name: comment_id in: query required: false schema: anyOf: - type: string format: uuid - type: 'null' description: Only tips on this comment. title: Comment Id description: Only tips on this comment. - name: limit in: query required: false schema: type: integer maximum: 100 minimum: 1 default: 20 title: Limit - name: offset in: query required: false schema: anyOf: - type: integer maximum: 100000 minimum: 0 - type: 'null' title: Offset - name: page in: query required: false schema: anyOf: - type: integer minimum: 1 - type: 'null' description: 1-indexed page number, an alternative spelling of ``offset``. Equivalent to ``offset = (page - 1) * limit``. Sending both is a 400 unless they agree. title: Page description: 1-indexed page number, an alternative spelling of ``offset``. Equivalent to ``offset = (page - 1) * limit``. Sending both is a 400 unless they agree. responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TipListResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/tips/comment/{comment_id}/stats: get: tags: - Tips summary: Get Comment Tip Stats description: 'Aggregate tip statistics for a single comment. Returns ``{comment_id, total_tips, total_sats}`` — same shape as the post-stats endpoint with the comment id substituted. Used by the comment thread to render the inline tip badge. Read-only; no auth, no rate limit.' operationId: get_comment_tip_stats_api_v1_tips_comment__comment_id__stats_get security: - HTTPBearer: [] parameters: - name: comment_id in: path required: true schema: type: string format: uuid title: Comment Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/CommentTipStatsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' components: schemas: TipListItem: properties: id: type: string title: Id amount_sats: type: integer title: Amount Sats tipper: anyOf: - additionalProperties: type: string type: object - type: 'null' title: Tipper recipient: additionalProperties: type: string type: object title: Recipient post_id: anyOf: - type: string - type: 'null' title: Post Id post_title: anyOf: - type: string - type: 'null' title: Post Title comment_id: anyOf: - type: string - type: 'null' title: Comment Id paid_at: anyOf: - type: string - type: 'null' title: Paid At type: object required: - id - amount_sats - tipper - recipient - post_id - post_title - comment_id - paid_at title: TipListItem description: One row in the ``GET /tips`` list. PostTipStatsResponse: properties: post_id: type: string title: Post Id total_tips: type: integer title: Total Tips total_sats: type: integer title: Total Sats type: object required: - post_id - total_tips - total_sats title: PostTipStatsResponse description: 'Body returned from ``GET /tips/post/{id}/stats`` — paid-tip aggregate for a single post.' CommentTipStatsResponse: properties: comment_id: type: string title: Comment Id total_tips: type: integer title: Total Tips total_sats: type: integer title: Total Sats type: object required: - comment_id - total_tips - total_sats title: CommentTipStatsResponse description: 'Body returned from ``GET /tips/comment/{id}/stats`` — paid-tip aggregate for a single comment.' TipListResponse: properties: total: type: integer title: Total offset: type: integer title: Offset limit: type: integer title: Limit has_more: type: boolean title: Has More tips: items: $ref: '#/components/schemas/TipListItem' type: array title: Tips type: object required: - total - offset - limit - has_more - tips title: TipListResponse description: Body returned from ``GET /tips`` — paginated list of paid tips. TipStatusResponse: properties: tip_id: type: string title: Tip Id status: type: string title: Status amount_sats: type: integer title: Amount Sats paid_at: anyOf: - type: string - type: 'null' title: Paid At type: object required: - tip_id - status - amount_sats - paid_at title: TipStatusResponse description: 'Body returned from ``POST /tips/{id}/check`` — current status of a tip invoice.' HTTPValidationError: properties: detail: items: $ref: '#/components/schemas/ValidationError' type: array title: Detail type: object title: HTTPValidationError ValidationError: properties: loc: items: anyOf: - type: string - type: integer type: array title: Location msg: type: string title: Message type: type: string title: Error Type input: title: Input ctx: type: object title: Context type: object required: - loc - msg - type title: ValidationError TipInvoiceResponse: properties: tip_id: type: string title: Tip Id payment_hash: type: string title: Payment Hash payment_request: type: string title: Payment Request amount_sats: type: integer title: Amount Sats expires_at: type: string title: Expires At type: object required: - tip_id - payment_hash - payment_request - amount_sats - expires_at title: TipInvoiceResponse description: 'Body returned from ``POST /tips/post/{id}`` and ``POST /tips/comment/{id}`` — BOLT11 invoice the caller pays out-of-band.' securitySchemes: _Compat403HTTPBearer: type: http scheme: bearer HTTPBearer: type: http scheme: bearer