{
  "openapi": "3.1.0",
  "info": {
    "title": "HAF FYP - For You Page",
    "description": "Personalized content feed API for the Hive blockchain. Provides algorithmically ranked post feeds, user profiles, and algorithm version management.",
    "license": {
      "name": "MIT License",
      "url": "https://opensource.org/license/mit"
    },
    "version": "2.0.0"
  },
  "externalDocs": {
    "description": "HAF FYP GitLab repository",
    "url": "https://gitlab.syncad.com/hive/haf_fyp"
  },
  "tags": [
    {
      "name": "Feed",
      "description": "Personalized and global content feeds"
    },
    {
      "name": "Algorithm",
      "description": "Algorithm version registry"
    },
    {
      "name": "User",
      "description": "User profile information"
    },
    {
      "name": "Other",
      "description": "Health and version endpoints"
    }
  ],
  "servers": [
    {
      "url": "/haf-fyp-api"
    }
  ],
  "components": {
    "schemas": {
      "fyp_ranking": {
        "type": "object",
        "x-sql-datatype": "object",
        "description": "This app's ranking data for the post (the FYP-specific signal).",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "personalized",
              "global"
            ],
            "description": "Which feed produced this entry - 'personalized' when the user has a precomputed personalized feed, 'global' when served the cold-start global fallback. Uniform across all entries in a single response."
          },
          "rank": {
            "type": "integer",
            "description": "Position in the feed (1-based)"
          },
          "final_score": {
            "type": "number",
            "description": "Composite ranking score [0, 1]"
          },
          "score_relevance": {
            "type": "number",
            "nullable": true,
            "description": "Semantic relevance to the user's interest vector (null in the global feed)"
          },
          "score_recency": {
            "type": "number"
          },
          "score_engagement": {
            "type": "number"
          },
          "score_credibility": {
            "type": "number"
          },
          "community_boost_applied": {
            "type": "boolean"
          },
          "boost_source": {
            "type": "string",
            "nullable": true,
            "description": "Which tier applied the community boost - 'subscribed', 'engaged', or null"
          },
          "post_id": {
            "type": "string",
            "description": "Internal post id (joins feed_events.post_id). Serialized as a STRING because the id exceeds JavaScript's 2^53 safe-integer limit - a JS Number would lose precision and break the telemetry join; echo it back verbatim in POST /v1/fyp/events."
          },
          "algorithm_version": {
            "type": "string",
            "nullable": true,
            "description": "Ranking algorithm version that produced this entry, for A/B analysis (null in the global feed)."
          }
        }
      },
      "feed_entry": {
        "type": "object",
        "x-sql-datatype": "object",
        "description": "A ranked post. The post itself is returned in the exact shape produced by Hivemind's bridge.get_ranked_posts (author, permlink, title, body, json_metadata, category, created/updated, payout / pending_payout / is_paidout / payout_at, author_reputation, author_role/title, children, net_rshares, active_votes, stats, beneficiaries, percent_hbd, max_accepted_payout, community / community_title, url, blacklists, reblogs, ...), so any front end that renders trending / hot / new can render this feed unchanged. A nested \"fyp\" object adds this app's ranking data.",
        "properties": {
          "author": {
            "type": "string"
          },
          "permlink": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "community": {
            "type": "string",
            "nullable": true
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "payout": {
            "type": "number"
          },
          "author_reputation": {
            "type": "number"
          },
          "children": {
            "type": "integer"
          },
          "json_metadata": {
            "type": "object"
          },
          "active_votes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "stats": {
            "type": "object"
          },
          "url": {
            "type": "string"
          },
          "fyp": {
            "$ref": "#/components/schemas/fyp_ranking"
          }
        },
        "example": {
          "author": "cryptoandcoffee",
          "permlink": "will-we-see-the-hydration-break-next-season",
          "category": "hive-115814",
          "title": "Will We See The Hydration / Advertising Break In The Premier League Next Season?",
          "body": "<center><img src=\"https://images.hive.blog/p/cover.png\"/></center>\n\nMarkdown body of the post...",
          "json_metadata": {
            "app": "leothreads/0.3",
            "format": "markdown",
            "tags": [
              "sports",
              "football",
              "premierleague",
              "advertising"
            ],
            "image": [
              "https://images.hive.blog/p/cover.png"
            ]
          },
          "created": "2026-06-19T16:33:57",
          "updated": "2026-06-19T16:33:57",
          "depth": 0,
          "children": 4,
          "community": "hive-115814",
          "community_title": "Sportsblock",
          "url": "/hive-115814/@cryptoandcoffee/will-we-see-the-hydration-break-next-season",
          "author_reputation": 84.5,
          "author_role": "guest",
          "author_title": "",
          "payout": 3.655,
          "pending_payout_value": "3.655 HBD",
          "author_payout_value": "0.000 HBD",
          "curator_payout_value": "0.000 HBD",
          "max_accepted_payout": "1000000.000 HBD",
          "percent_hbd": 10000,
          "is_paidout": false,
          "payout_at": "2026-06-26T16:33:57",
          "net_rshares": 50938299772643,
          "beneficiaries": [
            {
              "account": "leo.voter",
              "weight": 500
            }
          ],
          "blacklists": [],
          "reblogs": 0,
          "replies": [],
          "active_votes": [
            {
              "voter": "team",
              "rshares": 30982500342
            },
            {
              "voter": "roelandp",
              "rshares": 15794227518
            }
          ],
          "stats": {
            "hide": false,
            "gray": false,
            "total_votes": 127,
            "flag_weight": 0.0
          },
          "fyp": {
            "source": "personalized",
            "rank": 1,
            "final_score": 0.9178,
            "score_relevance": 0.62,
            "score_recency": 0.9926,
            "score_engagement": 0.9805,
            "score_credibility": 0.5679,
            "community_boost_applied": true,
            "boost_source": "subscribed"
          }
        }
      }
    }
  },
  "paths": {
    "/version": {
      "get": {
        "tags": [
          "Other"
        ],
        "summary": "Application version",
        "description": "Returns the git hash of the currently deployed version.",
        "operationId": "haf_fyp_endpoints.get_version",
        "responses": {
          "200": {
            "description": "Git hash string",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Other"
        ],
        "summary": "Health check",
        "description": "Returns service health and last processed block number.",
        "operationId": "haf_fyp_endpoints.get_health",
        "responses": {
          "200": {
            "description": "Health status",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "TABLE(status text, last_processed_block int, version text)",
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "last_processed_block": {
                      "type": "integer"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/algorithm": {
      "get": {
        "tags": [
          "Algorithm"
        ],
        "summary": "Active algorithm",
        "description": "Returns the currently active scoring algorithm version and its weights.",
        "operationId": "haf_fyp_endpoints.get_algorithm",
        "responses": {
          "200": {
            "description": "Active algorithm version",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "TABLE(version text, description text, weights jsonb, deployed_at timestamp)",
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string",
                      "example": "1.0.0"
                    },
                    "description": {
                      "type": "string"
                    },
                    "weights": {
                      "type": "object",
                      "additionalProperties": true,
                      "example": {
                        "relevance": 0.4,
                        "recency": 0.3,
                        "engagement": 0.2,
                        "credibility": 0.1,
                        "community_boost": 0.2
                      }
                    },
                    "deployed_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/algorithms": {
      "get": {
        "tags": [
          "Algorithm"
        ],
        "summary": "All algorithm versions",
        "description": "Lists all algorithm versions in the registry with their weights and active status.",
        "operationId": "haf_fyp_endpoints.list_algorithm_versions",
        "responses": {
          "200": {
            "description": "List of all algorithm versions",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "TABLE(version text, description text, weights jsonb, deployed_at timestamp, is_active boolean)",
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "version": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "weights": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "deployed_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "is_active": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/feed/{username}": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Personalized feed",
        "description": "Returns a ranked list of posts for the given user, ordered by final score. Each post is returned in the same shape as Hivemind's bridge.get_ranked_posts (the structure front ends already render for trending / hot / new), plus a nested \"fyp\" object carrying this app's ranking data, so the feed is a drop-in alternative content source. Falls back to the global feed until the ranker has cached a feed for the user.",
        "operationId": "haf_fyp_endpoints.get_personalized_feed",
        "parameters": [
          {
            "in": "path",
            "name": "username",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Hive account name"
          },
          {
            "in": "query",
            "name": "page",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "1-based page number"
          },
          {
            "in": "query",
            "name": "page-size",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Posts per page (max 20)"
          },
          {
            "in": "query",
            "name": "truncate_body",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Truncate each post body to this many characters (0 = full body)"
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked post list (empty array before first ranker cycle)",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "jsonb",
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/feed_entry"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/global": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Global feed",
        "description": "Returns the global unauthenticated feed, optionally filtered by community. Each post is returned in the same shape as Hivemind's bridge.get_ranked_posts, plus a nested \"fyp\" object with this app's ranking data, so the feed is a drop-in alternative content source for front ends.",
        "operationId": "haf_fyp_endpoints.get_global_feed",
        "parameters": [
          {
            "in": "query",
            "name": "page",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "1-based page number"
          },
          {
            "in": "query",
            "name": "page-size",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Posts per page (max 20)"
          },
          {
            "in": "query",
            "name": "community_id",
            "required": false,
            "schema": {
              "type": "integer",
              "default": null
            },
            "description": "Filter results to a specific community (optional)"
          },
          {
            "in": "query",
            "name": "truncate_body",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Truncate each post body to this many characters (0 = full body)"
          }
        ],
        "responses": {
          "200": {
            "description": "Global ranked post list",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "jsonb",
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/feed_entry"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/profile/{username}": {
      "get": {
        "tags": [
          "User"
        ],
        "summary": "User profile",
        "description": "Returns a user's computed interest profile - whether a vector has been built and which communities are tracked.",
        "operationId": "haf_fyp_endpoints.get_user_profile",
        "parameters": [
          {
            "in": "path",
            "name": "username",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Hive account name"
          }
        ],
        "responses": {
          "200": {
            "description": "User profile data (always one row; has_feed/has_vector are false if the user has not been ranked yet)",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "TABLE(has_feed boolean, has_vector boolean, posts_sampled int, top_communities int[], updated_at timestamp)",
                  "type": "object",
                  "properties": {
                    "has_feed": {
                      "type": "boolean",
                      "description": "True if the user has a precomputed personalized feed (vs the global fallback)"
                    },
                    "has_vector": {
                      "type": "boolean"
                    },
                    "posts_sampled": {
                      "type": "integer"
                    },
                    "top_communities": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/explain/{username}/{post_id}": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Score explanation",
        "description": "Returns a per-signal score breakdown for a specific post in a user's feed cache.",
        "operationId": "haf_fyp_endpoints.get_score_explanation",
        "parameters": [
          {
            "in": "path",
            "name": "username",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Hive account name"
          },
          {
            "in": "path",
            "name": "post_id",
            "required": true,
            "schema": {
              "x-sql-datatype": "BIGINT",
              "type": "integer"
            },
            "description": "Internal post ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Score breakdown (empty if post not in user's feed cache)",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "TABLE(post_id bigint, author varchar(16), permlink varchar(256), title text, final_score float, score_relevance float, score_recency float, score_engagement float, score_credibility float, community_boost_applied boolean, boost_source varchar(16), rank int, algorithm_version varchar(16), algorithm_weights jsonb, computed_at timestamp)",
                  "type": "object",
                  "properties": {
                    "post_id": {
                      "type": "integer"
                    },
                    "author": {
                      "type": "string"
                    },
                    "permlink": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "final_score": {
                      "type": "number"
                    },
                    "score_relevance": {
                      "type": "number"
                    },
                    "score_recency": {
                      "type": "number"
                    },
                    "score_engagement": {
                      "type": "number"
                    },
                    "score_credibility": {
                      "type": "number"
                    },
                    "community_boost_applied": {
                      "type": "boolean"
                    },
                    "boost_source": {
                      "type": "string",
                      "nullable": true,
                      "description": "Which tier applied the community boost - 'subscribed', 'engaged', or null"
                    },
                    "rank": {
                      "type": "integer"
                    },
                    "algorithm_version": {
                      "type": "string"
                    },
                    "algorithm_weights": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "computed_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/fyp/metrics": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Feed quality metrics",
        "description": "Aggregated feed-quality metrics from the telemetry rollup (feed_metrics_daily, #12), summed over a date range and grouped by algorithm_version / source / rank. Enables engagement-rate@rank, open rate, and average dwell/watch \u2014 and A/B comparison across algorithm_version. NON-AUTHORITATIVE: derived from the unauthenticated /v1/fyp/events log (username is client-claimed), so treat figures as directional and corroborate against on-chain engagement. Global-feed rows carry algorithm_version '' (no per-version A/B on the global feed).",
        "operationId": "haf_fyp_endpoints.get_feed_metrics",
        "parameters": [
          {
            "in": "query",
            "name": "from_date",
            "required": false,
            "schema": {
              "x-sql-datatype": "DATE",
              "type": "string",
              "format": "date",
              "default": null
            },
            "description": "Inclusive start date (YYYY-MM-DD). Default 30 days ago."
          },
          {
            "in": "query",
            "name": "to_date",
            "required": false,
            "schema": {
              "x-sql-datatype": "DATE",
              "type": "string",
              "format": "date",
              "default": null
            },
            "description": "Inclusive end date (YYYY-MM-DD). Default today."
          },
          {
            "in": "query",
            "name": "algorithm_version",
            "required": false,
            "schema": {
              "type": "string",
              "default": null
            },
            "description": "Filter to one algorithm version (optional)"
          },
          {
            "in": "query",
            "name": "source",
            "required": false,
            "schema": {
              "type": "string",
              "default": null
            },
            "description": "Filter to 'personalized' or 'global' (optional)"
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregated metric rows",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "jsonb",
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "algorithm_version": {
                        "type": "string"
                      },
                      "source": {
                        "type": "string"
                      },
                      "rank": {
                        "type": "integer"
                      },
                      "impressions": {
                        "type": "integer"
                      },
                      "opens": {
                        "type": "integer"
                      },
                      "actions": {
                        "type": "integer"
                      },
                      "videos": {
                        "type": "integer"
                      },
                      "engagement_rate": {
                        "type": "number",
                        "nullable": true,
                        "description": "actions (vote+comment+reblog+follow) / impressions"
                      },
                      "open_rate": {
                        "type": "number",
                        "nullable": true,
                        "description": "opens / impressions (closest to a click-through)"
                      },
                      "avg_dwell_ms": {
                        "type": "number",
                        "nullable": true
                      },
                      "avg_watch_ms": {
                        "type": "number",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}