{
  "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": "3.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. Global-sourced entries carry it too (#23); null only for entries written before that change."
          }
        }
      },
      "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": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "status": {
                        "type": "string",
                        "example": "ok"
                      },
                      "last_processed_block": {
                        "type": "integer"
                      },
                      "version": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sync-status": {
      "get": {
        "tags": [
          "Other"
        ],
        "summary": "Sync status",
        "description": "Last block consumed by the haf_fyp HAF context, and its timestamp. The uniform HAF-app freshness endpoint: same two keys and the same timestamp format as haf_stats' /sync-status. NOTE this reports REACHABILITY and the context head, not liveness -- a stalled app whose context sits at block 0 answers 200 with a null timestamp, which is a state observed in the wild (tests/ci_invariants.sql). Pair it with a block-freshness agent-check, as haf_api_node does for its own apps.",
        "operationId": "haf_fyp_endpoints.get_sync_status",
        "responses": {
          "200": {
            "description": "last_block_time is null when that block has no hafd.blocks row - i.e. nothing has been processed yet (there is no block 0), or the node is pruned and the context is behind the prune tail.",
            "content": {
              "application/json": {
                "schema": {
                  "x-sql-datatype": "JSON",
                  "type": "object",
                  "required": [
                    "last_block_num",
                    "last_block_time"
                  ],
                  "properties": {
                    "last_block_num": {
                      "type": "integer",
                      "description": "last block consumed by the haf_fyp HAF context"
                    },
                    "last_block_time": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "UTC timestamp of that block; null if it has no hafd.blocks row"
                    }
                  }
                },
                "example": {
                  "last_block_num": 109603308,
                  "last_block_time": "2026-09-03T22:46:54"
                }
              }
            }
          },
          "500": {
            "description": "Unavailable, and the two cases are distinguished by message rather than by status. Either the HAF instance is still in massive sync (hafd.blocks' primary key is dropped for the duration, so the call fails fast instead of seq-scanning the largest table in the database), or the HAF context is not registered on this node. Both are raised as SQLSTATE 55000 -- the code every other HAF app uses for the massive-sync guard -- which PostgREST maps to 500. 503 would be the better status for a temporary condition, but it is not what the ecosystem does, and one app diverging is worse than the imperfection; changing it belongs everywhere at once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "PostgreSQL SQLSTATE, '55000' for both cases"
                    },
                    "message": {
                      "type": "string"
                    },
                    "details": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "hint": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "code": "55000",
                  "message": "HAF instance is not ready (massive sync in progress)",
                  "details": null,
                  "hint": null
                }
              }
            }
          }
        }
      }
    },
    "/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": "array",
                  "items": {
                    "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": "array",
                  "items": {
                    "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": "array",
                  "items": {
                    "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/feed-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 responses now carry the algorithm_version that produced them (#23), so clients that echo it back yield attributed global rows; the server never derives this column, so anything a client omits still lands in the '' bucket (feed_metrics_daily.algorithm_version is NOT NULL DEFAULT ''). Rows recorded before #23 sit in '' for the same reason and are not backfilled. The global feed is identical for every viewer, so global-source attribution supports longitudinal before/after comparison rather than concurrent A/B arms.",
        "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
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}