# Ethos API

Ethos is an onchain reputation network. This API exposes credibility scores, reviews, vouches, profiles, XP and reputation markets over REST.

Base URL: `https://api.ethos.network/api/v2`. Send an `X-Ethos-Client` header naming your app, for example `my-app@1.0.0`. Requests without it share one strict rate limit across all anonymous callers.

Most read endpoints are public. Endpoints that require authentication take either a long-lived API key in the `X-Ethos-Api-Key` header, available to profiles with API key access enabled, or a short-lived Privy session token in the `Authorization: Bearer` header. Do not send an API key as a Bearer token. Operations that require the `SessionCookie` scheme are for first-party browser sessions only: they take the HttpOnly cookies set by `POST /auth/exchange`, which itself takes the Privy Bearer token, and mutations are refused with 403 unless the request comes from an Ethos origin. See the API Keys guide on developers.ethos.network.

A **user** is the person record Ethos scores; a **profile** is the on-chain identity a user gets when they join. A user can exist without a profile. Most endpoints accept a **userkey**, a string that identifies a user by any linked account:

- `address:0x…` an Ethereum address
- `profileId:123` an Ethos profile id
- `service:x.com:123456789` an X account by numeric id
- `service:x.com:username:handle` an X account by handle (score, NFT, endorsement and connection lookups; user lookups take the handle as a path parameter instead)
- `service:discord:<id>`, `service:farcaster:<id>`, `service:telegram:<id>`, `service:github:<id>` other linked accounts

Scores range from 0 to 2800 and map to a named level from untrusted to renowned. List endpoints are paginated one of two ways, shown by their response shape: offset pagination takes `limit` and `offset` and returns `{ values, total, limit, offset }`; cursor pagination takes `limit` and `cursor` and returns `{ values, nextCursor }`.

## Activities

Feed items: reviews, vouches, slashes, replies and other on-chain or social actions, fetched by type and id, by transaction hash, or as a feed for a user or project.

### GET /activities/{activityType}/{id}
Get activity by id

Parameters:
- activityType (path, required): "attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip"
- id (path, required): number

Response: object

### POST /activities/bulk
Get activities by type and id

Body: { activityType: "attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip", id: number }

Response: object[]

### GET /activities/{activityType}/tx/{txHash}
Get activity by transaction hash

Parameters:
- activityType (path, required): "attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip"
- txHash (path, required): string

Response: object

### GET /activities/userkey
[Deprecated] Get activities by userkey (author or subject). Use /activities/profile/{given,received,all} instead.

Parameters:
- userkey (query, required): string
- direction (query, optional): "author" | "subject"
- activityType (query, optional): ("PROFILE" | "ATTESTATION" | "INVITATION" | "VOUCH" | "UNVOUCH" | "VOUCH_V2" | "UNVOUCH_V2" | "VOUCH_V2_DECREASE" | "VOUCH_V2_INCREASE" | "SLASH" | "REVIEW" | "REVIEW_ARCHIVED" | "MARKET" | "MARKET_VOTE" | "MARKET_V2_TRADE" | "VOTE" | "REPLY" | "CLAIM" | "PROJECT" | "BROKER" | "BOND" | "XP_TIP" | "KARMA_TIP")[]
- orderBy (query, optional): "createdAt" | "totalVotes" | "netVotes" | "replyCount" | "authorScore" | "subjectScore" | "votes"
- sort (query, optional): "asc" | "desc"
- reviewScore (query, optional): "NEGATIVE" | "NEUTRAL" | "POSITIVE" | ("NEGATIVE" | "NEUTRAL" | "POSITIVE")[]
- includeArchived (query, optional): boolean
- excludeSpam (query, optional): boolean
- excludeBoostedReviews (query, optional): boolean
- limit (query, optional): number
- offset (query, optional): number

Response: object[]

### POST /activities/feed
Get activities optimized for the feed

Body: { cache?: boolean, dayRange?: number | null, excludeSpam?: boolean, excludeUnverified?: boolean, filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" }, limit?: number, offset?: number }

Response: { values: object[], total: number, limit: number, offset: number }

### POST /activities/notifications
Get activities for notifications feed

Requires authentication.

Body: { filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], excludeHistorical?: boolean, excludeSpam?: boolean, excludeUnverified?: boolean, pagination: { limit?: number, offset?: number }, orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" } }

Response: { values: object[], total: number, limit: number, offset: number, offsets?: Record<string, number>, counts?: Record<string, number> }

### POST /activities/profile/given
Get activities authored by the specified user, plus EthosAgent saves credited to them

Body: { userkey: string, filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], excludeHistorical?: boolean, excludeSpam?: boolean, excludeUnverified?: boolean, excludeBoostedReviews?: boolean, orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" }, reviewScore?: "NEGATIVE" | "NEUTRAL" | "POSITIVE" | ("NEGATIVE" | "NEUTRAL" | "POSITIVE")[], minAuthorScore?: number, limit?: number, offset?: number }

Response: { values: object[], total: number, limit: number, offset: number }

### POST /activities/profile/saved-posts
[Deprecated] Always empty; saves now come back from the Given and All routes.

Body: { userkey: string, cursor?: number, limit?: number }

Response: { nextCursor: number | null, values: object[] }

### POST /activities/profile/received
Get activities where the specified user is the subject

Body: { userkey: string, filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], excludeHistorical?: boolean, excludeSpam?: boolean, excludeUnverified?: boolean, excludeBoostedReviews?: boolean, orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" }, reviewScore?: "NEGATIVE" | "NEUTRAL" | "POSITIVE" | ("NEGATIVE" | "NEUTRAL" | "POSITIVE")[], minAuthorScore?: number, limit?: number, offset?: number }

Response: { values: object[], total: number, limit: number, offset: number }

### POST /activities/profile/all
Get all activities (authored & received) for a user, plus EthosAgent saves credited to them

Body: { userkey: string, filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], excludeHistorical?: boolean, excludeSpam?: boolean, excludeUnverified?: boolean, excludeBoostedReviews?: boolean, orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" }, reviewScore?: "NEGATIVE" | "NEUTRAL" | "POSITIVE" | ("NEGATIVE" | "NEUTRAL" | "POSITIVE")[], minAuthorScore?: number, limit?: number, offset?: number }

Response: { values: object[], total: number, limit: number, offset: number }

### POST /activities/project
Get project-related activities by project

Body: { userkey: string, filter?: ("attestation" | "broker-post" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vote" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "xp-tip")[], excludeHistorical?: boolean, excludeSpam?: boolean, excludeUnverified?: boolean, excludeBoostedReviews?: boolean, orderBy?: { field: "timestamp" | "votes" | "comments" | "score" | "controversial" | "hot" | "distance", direction: "asc" | "desc" }, reviewScore?: "NEGATIVE" | "NEUTRAL" | "POSITIVE" | ("NEGATIVE" | "NEUTRAL" | "POSITIVE")[], minAuthorScore?: number, limit?: number, offset?: number }

Response: { values: object[], total: number, limit: number, offset: number }

### GET /activities/slash/{slashId}/impacted-supporters
Get paginated list of supporters impacted by a slash

Parameters:
- slashId (path, required): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … }, penaltyAmount: number })[], total: number, limit: number, offset: number }

## Ai Images

AI-generated images for slashes and broker posts: read the image currently selected for an entity.

### GET /ai-images/selected
Get selected AI image job for entity

Parameters:
- entityType (query, required): "SLASH" | "BROKER"
- entityId (query, required): number

Response: { id: number, jobType: "SLASH" | "BROKER", entityId: number, status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED", isSelected: boolean, error: string | null, metadata: unknown | null, createdAt: string, updatedAt: string, completedAt: string | null, AiImages: { id: number, jobId: number, fileName: string, imageUrl: string, metadata: unknown, createdAt: string }[] } | null

## Announcements

In-app announcements shown to users and the endpoint to mark them viewed.

### GET /announcements/active
Get active announcements for current user (works for anonymous users)

Response: ({ id: number, title: string, description: string, imageUrl: string | null, actionButtonLabel: string | null, actionButtonUrl: string | null, startDateTime: string, endDateTime: string, status: "ACTIVE" | "INACTIVE", createdAt: string, updatedAt: string, authorAvatarUrl: string | null })[]

### POST /announcements/{id}/viewed
Mark an announcement as viewed

Requires authentication.

Parameters:
- id (path, required): number

Response: { ok: true }

## Api Keys

Create, list and revoke long-lived API keys for bots, scripts and integrations. Keys are minted with a SIWE signature.

### POST /api-keys
Create API key

Authenticates via SIWE signature and creates an API key for the associated profile.

Body: { address: string, expiresAt?: string, message: string, name: string, signature: string }

Response: { expiresAt: string, id: string, name: string, token: string }

### GET /api-keys
List API keys

Returns all API keys for the authenticated profile.

Requires authentication.

Response: ({ address: string, createdAt: string, expiresAt: string, id: string, lastUsedAt: string | null, name: string, profileId: number })[]

### DELETE /api-keys/{id}
Revoke API key

Revokes an API key. Only the key owner can revoke it.

Requires authentication.

Parameters:
- id (path, required): string

Response: { ok: true }

## Apps

Registry of third-party apps built on Ethos, as shown on the Built on Ethos page.

### GET /apps
List all applications with pagination and filtering

Retrieve a paginated list of applications built on Ethos. Supports filtering by app type (APP, INTEGRATION, AGENT) and status (ACTIVE, INACTIVE).

Parameters:
- appType (query, optional): "APP" | "INTEGRATION" | "AGENT"
- status (query, optional): "ACTIVE" | "INACTIVE"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, name: string, appType: "APP" | "INTEGRATION" | "AGENT", authorUserId: number, link: string, description: string, pictureUrl?: string, status: "ACTIVE" | "INACTIVE", createdAt: string, updatedAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### GET /apps/{id}
Get a specific app by ID

Parameters:
- id (path, required): number

Response: { id: number, name: string, appType: "APP" | "INTEGRATION" | "AGENT", authorUserId: number, link: string, description: string, pictureUrl?: string, status: "ACTIVE" | "INACTIVE", createdAt: string, updatedAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } } }

### GET /apps/by-type/{appType}
Get apps by type

Parameters:
- appType (path, required): "APP" | "INTEGRATION" | "AGENT"

Response: ({ id: number, name: string, appType: "APP" | "INTEGRATION" | "AGENT", authorUserId: number, link: string, description: string, pictureUrl?: string, status: "ACTIVE" | "INACTIVE", createdAt: string, updatedAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } } })[]

## Benefits

Perks unlocked by credibility score or for a limited time, including exclusive access codes and their claim flow.

### GET /benefits/limited-time
List limited-time benefits with pagination and filtering

Retrieve a paginated list of limited-time benefits (perks and codes). Supports filtering by status and sorting.

Parameters:
- status (query, optional): "ACTIVE" | "INACTIVE"
- expirationStatuses (query, optional): ("ACTIVE" | "UPCOMING" | "EXPIRED")[]
- sortBy (query, optional): "createdAt" | "expirationDate"
- sortDirection (query, optional): "asc" | "desc"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, slug: string, type: "LIMITED_TIME_CODE", shortDescription: string, longDescription: string, logoImageUrl?: string, carouselImageUrl?: string, secondaryLinkLabel?: string, secondaryLinkUrl?: string, status: "ACTIVE" | "INACTIVE", issuer?: {…}, isLocked: boolean, createdAt: string, updatedAt: string, qualifications: {…}, startDate: string, expirationDate: string } | { id: number, slug: string, type: "LIMITED_TIME_PERK", shortDescription: string, longDescription: string, logoImageUrl?: string, carouselImageUrl?: string, secondaryLinkLabel?: string, secondaryLinkUrl?: string, status: "ACTIVE" | "INACTIVE", issuer?: {…}, isLocked: boolean, createdAt: string, updatedAt: string, qualifications: {…}, primaryLinkLabel: string, primaryLinkUrl: string, startDate: string, expirationDate: string })[], total: number, limit: number, offset: number }

### GET /benefits/score-based
List score-based benefits with pagination and filtering

Retrieve a paginated list of score-based benefits. By default, returns all benefits. Optionally filter by status, multiple category IDs, and user score.

⚠️ **IMPORTANT**: Due to OpenAPI UI limitations, the `categoryIds` parameter requires at least 2 items to work correctly. Single category selection will result in a validation error.

Parameters:
- status (query, optional): "ACTIVE" | "INACTIVE"
- categoryIds (query, optional): number[]
- score (query, optional): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, slug: string, type: "SCORE_BASED", shortDescription: string, longDescription: string, logoImageUrl?: string, carouselImageUrl?: string, secondaryLinkLabel?: string, secondaryLinkUrl?: string, status: "ACTIVE" | "INACTIVE", issuer?: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, isLocked: boolean, createdAt: string, updatedAt: string, qualifications: {…}[], isHumanVerifiedOnly: boolean, primaryLinkLabel: string, primaryLinkUrl: string, categoryId: number, category?: { id: number, name: string, displayName: string, sortOrder: number, isActive: boolean, createdAt: string, updatedAt: string } })[], total: number, limit: number, offset: number }

### GET /benefits/categories
Get all categories

Parameters:
- activeOnly (query, optional): boolean

Response: { id: number, name: string, displayName: string, sortOrder: number, isActive: boolean, createdAt: string, updatedAt: string }[]

### GET /benefits/{benefitId}/codes/info
Get benefit code info

Get information about exclusive access codes for a benefit. Returns code availability and the current user's claimed code if logged in.

Parameters:
- benefitId (path, required): number

Response: { hasAccessCodes: boolean, totalCodes: number, claimedCodes: number, availableCodes: number, userClaimedCode: string | null }

### POST /benefits/{benefitId}/codes/claim
Claim an exclusive access code

Requires authentication.

Parameters:
- benefitId (path, required): number

Response: { code: string | null, alreadyClaimed: boolean }

## Broker

Broker posts: buy, sell, hire, for-hire and bounty posts between users, with watch, report and completion tracking.

### GET /broker/posts
List broker posts with filtering and pagination

Retrieve a paginated list of broker posts. Supports filtering by type (SELL, BUY, HIRE, FOR_HIRE), status, tags, author minimum score, and search functionality for title/description. Results can be sorted by newest, score, top (most votes), or expiresAt (expiring soon). When level is not specified (ALL), minScore acts as "author score >= minScore OR premium level". For BASIC or PREMIUM level, minScore applies normally.

Parameters:
- excludeUnverified (query, optional): boolean
- level (query, optional): "BASIC" | "PREMIUM"
- minScore (query, optional): number
- recentlyCompletedDays (query, optional): number
- search (query, optional): string
- sortBy (query, optional): "newest" | "score" | "top" | "expiresAt" | "hot"
- statuses (query, optional): ("OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED")[]
- tags (query, optional): string | string[]
- type (query, optional): "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number })[], total: number, limit: number, offset: number }

### GET /broker/me/posts
Get current user's broker posts

Retrieve a paginated list of broker posts created by the current authenticated user. Supports filtering by type and status.

Requires authentication.

Parameters:
- statuses (query, optional): ("OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED")[]
- type (query, optional): "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY"
- includeArchived (query, optional): boolean
- sortBy (query, optional): "newest" | "score" | "top" | "expiresAt" | "hot"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number })[], total: number, limit: number, offset: number }

### GET /broker/posts/{id}
Get a specific broker post by ID

Retrieve detailed information about a specific broker post including author information.

Parameters:
- id (path, required): number

Response: { id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number }

### GET /broker/posts/tx/{txHash}
Get broker post by transaction hash

Retrieve a broker post by the transaction hash that created or updated it. This endpoint will spot-process the transaction if needed to ensure the latest data is available.

Parameters:
- txHash (path, required): string

Response: { id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number }

### PUT /broker/posts/{id}/status
Update broker post status/archive fields (author only)

Update off-chain fields like status, archived, and image URL that are not stored on blockchain. When changing status from EXPIRED to OPEN for BASIC posts, automatically resets expiration date to 30 days from now.

Requires authentication.

Parameters:
- id (path, required): number

Body: { status?: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", isArchived?: boolean, imageUrl?: string | null, transactedWithUserId?: number }

Response: { id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number }

### PUT /broker/posts/{id}/archive
Archive a broker post (author only)

Requires authentication.

Parameters:
- id (path, required): number

Response: { id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number }

### GET /broker/author/{profileId}/posts
Get posts by specific author

Retrieve a paginated list of broker posts created by a specific author. Supports filtering by type and status. Only shows non-archived posts.

Parameters:
- profileId (path, required): number
- statuses (query, optional): ("OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED")[]
- type (query, optional): "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY"
- includeArchived (query, optional): boolean
- sortBy (query, optional): "newest" | "score" | "top" | "expiresAt" | "hot"
- limit (query, optional): number
- offset (query, optional): number
- recentlyCompletedDays (query, optional): number

Response: { values: ({ id: number, authorProfileId: number, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", title: string, description: string, cost: string, tags: string[], level: "BASIC" | "PREMIUM", status: "OPEN" | "COMPLETED" | "CLOSED" | "EXPIRED", imageUrl?: string, isArchived: boolean, createdAt: string, updatedAt: string, expiresAt: string, author: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, completedAt?: string, replySummary: { count: number, participated: boolean }, watcherCount: number })[], total: number, limit: number, offset: number }

### GET /broker/author/{profileId}/completed-summary
Get completed posts summary for a user

Returns the total count of completed broker posts and the users transacted with.

Parameters:
- profileId (path, required): number
- transactedUsersLimit (query, optional): number

Response: { total: number, totalTransactedUsersCount: number, transactedUsers: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } })[] }

### POST /broker/posts/{id}/report
Report a broker post for inappropriate content

Report a broker post for spam, inappropriate content, fraud, harassment, or other issues. Requires authentication.

Requires authentication.

Parameters:
- id (path, required): number

Body: { reason: "SPAM" | "INAPPROPRIATE_CONTENT" | "FRAUD" | "HARASSMENT" | "OTHER", description?: string }

Response: { success: boolean, message: string }

### POST /broker/preview-premium-image
Preview AI image for broker post

Generates a preview image for a broker post using the provided content and author profile. Returns the image directly without creating a job.

Requires authentication.

Body: { title: string, description: string, type: "SELL" | "BUY" | "HIRE" | "FOR_HIRE" | "BOUNTY", position?: string }

Response: { imageUrl: string, prompt: string }

### POST /broker/posts/{postId}/watch
Watch a broker post

Requires authentication.

Parameters:
- postId (path, required): number

Response: { success: boolean }

### DELETE /broker/posts/{postId}/watch
Unwatch a broker post

Requires authentication.

Parameters:
- postId (path, required): number

Response: { success: boolean }

### GET /broker/posts/{postId}/watch
Get watch status for a broker post

Requires authentication.

Parameters:
- postId (path, required): number

Response: { isWatching: boolean, watcherCount: number }

## Categories

Admin-curated groups of users, used for leaderboards and daily tasks: list groups, their summaries and their members.

### GET /categories
List categories

Parameters:
- showOnLeaderboard (query, optional): boolean
- showInDailyService (query, optional): boolean
- withUserCount (query, optional): boolean
- limit (query, optional): number
- offset (query, optional): number

Response: { categories: { id: number, slug?: string, name: string, description?: string, showOnLeaderboard: boolean, showInDailyService: boolean, showInDailyBounty: boolean, bannerImageUrl?: string, userCount: number }[], total: number }

### GET /categories/{id}
Get a specific category by ID

Parameters:
- id (path, required): number

Response: { id: number, slug?: string, name: string, description?: string, showOnLeaderboard: boolean, showInDailyService: boolean, showInDailyBounty: boolean, bannerImageUrl?: string, userCount: number }

### GET /categories/summaries/bulk
Get summaries for multiple categories

Parameters:
- showOnLeaderboard (query, optional): boolean
- showInDailyService (query, optional): boolean
- topUsersLimit (query, optional): number

Response: ({ id: number, slug?: string, name: string, description?: string, showOnLeaderboard: boolean, showInDailyService: boolean, showInDailyBounty: boolean, bannerImageUrl?: string, topUsers: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} })[], userStats: { totalUsers: number, averageScore: number, totalXp: number, averageXpTotal: number, totalXpStreakDays: number, averageXpStreakDays: number, totalReviewReceivedPositive: number, averageReviewReceivedPositive: number, totalReviewReceivedNegative: number, averageReviewReceivedNegative: number, totalReviewReceivedNeutral: number, averageReviewReceivedNeutral: number, totalVouchReceivedCount: number, averageVouchReceivedCount: number, totalVouchGivenCount: number, averageVouchGivenCount: number, totalVouchReceivedAmountWei: number, averageVouchReceivedAmountWei: number, totalVouchGivenAmountWei: number, averageVouchGivenAmountWei: number } })[]

### GET /categories/{categoryId}/users
Get users belonging to a category

Parameters:
- categoryId (path, required): number
- limit (query, optional): number
- offset (query, optional): number
- searchText (query, optional): string
- orderBy (query, optional): "createdAt" | "score" | "displayName" | "xpTotal"

Response: { users: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … }, addedAt: string })[], total: number }

## Chains

Blockchains that projects can list, plus the current Base gas price.

### GET /chains
List all available chains

Response: { chains: ({ id: number, name: string, url: string | null, iconUrl: string | null })[] }

### GET /chains/base/gas-price
Get current Base gas price

> ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Response: { wei: number, level: "low" | "medium" | "high", updatedAt: string }

## Contributions

Daily contribution tasks, streaks and bounties that earn XP. A streak is consecutive days with completed contributions.

### GET /bounty/today
Get today’s daily bounty

Requires authentication.

Response: { bounty: { id: number, targetUserId: number, date: string, source: "QUEUE" | "RANDOM", xpReward: number } | null, targetUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } } | null, completed: boolean, completionCount: number }

### GET /contributions/history
Get user contribution history

Requires authentication.

Parameters:
- from (query, required): string
- to (query, optional): string

Response: { history: ({ date: string, tasks: number, pending: number, forgiven: boolean, status: "completed" | "forgiven" | "incomplete" | "skipped" })[], earliestDate: string | null }

### GET /contributions/latest-missed-day
Get the latest missed contribution day for the current user

Requires authentication.

Response: { date: string | null }

### POST /contributions/forgive-self
Forgive the most recent missed contribution day for yourself (costs XP)

Requires authentication.

Response: { ok: boolean, message: string, code?: "XP_FROZEN", forgivenDate?: string, updated?: number }

### GET /contributions/dailies
Get the current dailies for the authenticated user

Requires authentication.

Response: { aiSummaries: Record<string, { activityCount: number, generatedAt: string | null, summary: string | null, topAuthors: {…}[], uniqueAuthorsCount: number }>, bundles: object[], humanVerifications: Record<string, { bonds: …[], connectionDegree: …, createdAt: string | null, expiresAt: string | null, hasSelfDeclaredBond: boolean, id: …, selfDeclaredBondId: …, status: …, subject: {…}, verifiedAt: string | null, viewerFollowsThem: boolean } | null>, reviews: Record<string, object>, users: Record<string, { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }> }

## Curators

The Ethos curators program: the current round’s board of people paid in $WHUF for bringing others to Ethos from X.

### GET /external/curators/me
Get the signed-in profile’s line on the Ethos curators board

Requires authentication.

Response: { avatarUrl: string | null, name: string, points: number, rank: number | null, username: string, clicks: number, credibleReplies: number, posts: number } | null

### GET /external/curators/standings
Get the Ethos curators board: the current season and its top three

Response: { season: { curatorCount: number, endsAt: string, name: string, status: "final" | "open" | "review" }, top: ({ avatarUrl: string | null, name: string, points: number, rank: …, username: string })[] } | null

## Endorsements

Users the viewer knows who have vouched for or positively reviewed a target user.

### GET /endorsements/{targetUserkey}
Get endorsers of a target user that the viewer knows

Parameters:
- targetUserkey (path, required): string
- viewerProfileId (query, optional): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ activityId: number, endorserProfileId: number, endorsementType: "vouch" | "review", sourceId: number, sourceToken: "eth" | "whuf" | null, createdAt: string, connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none", endorser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], summary: { total: number, mutualVouches: number, youVouched: number, vouchedForYou: number, mutualReviews: number, youReviewed: number, reviewedYou: number }, limit: number, offset: number, total: number }

## Ens

Resolve ENS names to addresses and back.

### GET /external/ens/{address}
Get ENS details by Ethereum address

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { address: string | null, avatar: string | null, name: string | null }

### GET /external/ens/by-name/{name}
Get ENS details by ENS name

Parameters:
- name (path, required): string

Response: { address: string | null, avatar: string | null, name: string | null }

## Event Markers

Dated events overlaid on charts, such as launches or incidents.

### GET /event-markers/active
Get currently active event markers (date <= now <= endDate)

Response: ({ id: number, date: string, description: string, endDate: string | null, header: string | null, linkLabel: string | null, linkUrl: string | null, tagText: string | null, tagUrl: string | null, createdAt: string, updatedAt: string })[]

### GET /event-markers
Get event markers within a date range

Parameters:
- endDate (query, required): string
- startDate (query, required): string

Response: ({ id: number, date: string, description: string, endDate: string | null, header: string | null, linkLabel: string | null, linkUrl: string | null, tagText: string | null, tagUrl: string | null, createdAt: string, updatedAt: string })[]

## Exchange Rates

Current ETH and WHUF prices in USD.

### GET /exchange-rates/eth-price-in-usd
Get the current price of ETH in USD

Response: { price: number }

### GET /exchange-rates/whuf-price-in-usd
Get the current price of WHUF in USD

The WHUF spot price in USD, cached for a maximum of 10 minutes.

Response: { price: number }

## Feature Views

Records that the current user has dismissed a one-time overlay.

### GET /feature-views/{featureKey}
Check if the current user has viewed a feature

Requires authentication.

Parameters:
- featureKey (path, required): "human_verified_overlay"

Response: { viewed: boolean }

### POST /feature-views/{featureKey}/viewed
Mark a feature as viewed for the current user

Requires authentication.

Parameters:
- featureKey (path, required): "human_verified_overlay"

Response: { ok: true }

## Human Verification

Humanity bonds: verified users with an established score stake credibility on another account being a real person. Read verification state, find verifiers or request verification.

### GET /human-verification/search
Search human verifications

Returns human verifications matching the given search criteria.

Parameters:
- connectionScope (query, optional): "all" | "following" | "network"
- followsScope (query, optional): "partiallyVerified" | "all"
- minScore (query, optional): number
- search (query, optional): string
- sortBy (query, optional): "newest" | "proximity" | "random" | "score" | "verifications"
- sortOrder (query, optional): "asc" | "desc"
- status (query, optional): "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | ("REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING")[]
- minVerifications (query, optional): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ bonds: {…}[], connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none" | null, createdAt: string | null, expiresAt: string | null, hasSelfDeclaredBond: boolean, id: number | null, selfDeclaredBondId: number | null, status: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, verifiedAt: string | null, viewerFollowsThem: boolean })[], total: number, limit: number, offset: number }

### GET /human-verification/my-verifications
Get human verifications created by the current user

Returns human verifications where the subject has a bond created by the authenticated user.

Requires authentication.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ bonds: {…}[], connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none" | null, createdAt: string | null, expiresAt: string | null, hasSelfDeclaredBond: boolean, id: number | null, selfDeclaredBondId: number | null, status: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, verifiedAt: string | null, viewerFollowsThem: boolean })[], total: number, limit: number, offset: number }

### GET /human-verification/verifications
Get human verification bonds

Returns all HUMAN bonds with subject and actor info, ordered by creation date descending.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ bond: { id: number, bondType: "HUMAN_VERIFICATION", statement: string, amount: string, amountType: string, expiration: string, authorProfileId: number, subjectUserId: …, archived: boolean, archivedAt: string | null, archiveReason: string | null, createdAt: string, updatedAt: string, metadata: {…}, author: {…}, links: {…} }, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### POST /human-verification/request
Request human verification

Creates a human verification request for the authenticated user. Idempotent - succeeds if already requested.

Requires authentication.

Response: { success: boolean }

### GET /human-verification/verifiers
Search eligible human verifiers

Returns eligible verifiers sorted by proximity to the current user. Used by requesters to find verifiers to reach out to.

Requires authentication.

Parameters:
- search (query, optional): string
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none" | null, hasNudged: boolean, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### GET /human-verification/{subjectUserkey}
Get human verification info for a subject

Returns the human verification status, expiration, and any bonds where the subject is being verified. Accepts profile-userkeys (`profileId:<n>`) or X-account userkeys (`service:x.com:<id>`).

Parameters:
- subjectUserkey (path, required): string

Response: { bonds: ({ id: number, bondType: "HUMAN_VERIFICATION", statement: string, amount: string, amountType: string, expiration: string, authorProfileId: number, subjectUserId: …, archived: boolean, archivedAt: string | null, archiveReason: string | null, createdAt: string, updatedAt: string, metadata: {…}, author: {…}, links: {…}, translation?: … })[], connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none" | null, createdAt: string | null, expiresAt: string | null, hasSelfDeclaredBond: boolean, id: number | null, selfDeclaredBondId: number | null, status: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }, verifiedAt: string | null, viewerFollowsThem: boolean } | null

### POST /signatures/humanity-bond
Create a signature for a human verification bond

Creates a signed payload for submitting a HUMAN bond to the blockchain. The caller must then submit the bond to the blockchain themselves.

Requires authentication.

Body: { subjectProfileId?: number, subjectUserkey?: string, verificationMethod: "IRL" | "VIDEO" | "VOICE", comment: string }

Response: { statement: string, expiration: string, amount: string, amountType: string, metadata: string, randValue: number, signature: string, bondType: "HUMAN_VERIFICATION" }

### POST /signatures/self-declared-humanity-bond
Create a signature for a self-declared human bond

Creates a signed payload for submitting a self-declared HUMAN bond to the blockchain. Any user with an Ethos profile can create one. Limited to one per profile.

Requires authentication.

Response: { statement: string, expiration: string, amount: string, amountType: string, metadata: string, randValue: number, signature: string, bondType: "SELF_DECLARED_HUMAN" }

## Internal

Endpoints used by Ethos’s own frontends. Not guaranteed to be stable and may change without notice.

### GET /internal/users/embedded-wallet/{address}
Get user by embedded wallet

> ⚠️ **Warning:** This is an internal endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }, profile: { id: number, archived: boolean, createdAt: number, updatedAt: number, invitesAvailable: number, invitedBy: number } | null, inviterUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null, allAddresses: { addresses: string[], primaryAddress?: string, embeddedWallet?: string, smartWallet?: string }, attestations: ({ id: number, hash: string, archived: boolean, profileId: number, service: "discord" | "github" | "pump.fun" | "solana" | "telegram" | "x.com", account: string, createdAt: number, extra: {…} })[], isValidator: boolean, validatorAcquiredAt: string | null, verifierStatus: "verifier" | "unqualified" | "blocked", verifierRequirements: { isHumanVerified: boolean, meetsScoreThreshold: boolean }, xUsernameHistory: ({ username: string, changeObservedAt: string | null })[] } | null

### GET /internal/users/{userkey}
Get user by userkey

> ⚠️ **Warning:** This is an internal endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- userkey (path, required): string

Response: { user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }, profile: { id: number, archived: boolean, createdAt: number, updatedAt: number, invitesAvailable: number, invitedBy: number } | null, inviterUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null, allAddresses: { addresses: string[], primaryAddress?: string, embeddedWallet?: string, smartWallet?: string }, attestations: ({ id: number, hash: string, archived: boolean, profileId: number, service: "discord" | "github" | "pump.fun" | "solana" | "telegram" | "x.com", account: string, createdAt: number, extra: {…} })[], isValidator: boolean, validatorAcquiredAt: string | null, verifierStatus: "verifier" | "unqualified" | "blocked", verifierRequirements: { isHumanVerified: boolean, meetsScoreThreshold: boolean }, xUsernameHistory: ({ username: string, changeObservedAt: string | null })[] } | null

### GET /internal/trending-users
Get trending users for homepage

> ⚠️ **Warning:** This is an internal endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- excludeUnverified (query, optional): boolean
- limit (query, optional): number

Response: { values: ({ source: "review" | "vouch" | "category", user: {…}, stats: {…} } | { source: "verify", user: {…}, verificationCount: number } | { source: "marketV2", market: {…} })[] }

### GET /internal/trending-news
Get trending news like top vouchers

> ⚠️ **Warning:** This is an internal endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Response: { topVouches: object[], topContributors: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … }, points: number })[], topScoreMovers: ({ user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, scoreChange: number })[], topTradeMovers: ({ marketOnchainId: number, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, priceChangePercent: number })[], pastSlashes: ({ id: number, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, slashVotePercent: number })[] }

## Invitations

Invitations to join Ethos: pending invites for an address, invite eligibility and the accepted-invite tree.

### GET /invitations
Get a list of invitations

Parameters:
- senderProfileId (query, optional): number
- status (query, optional): ("ACCEPTED" | "INVITED" | "ACCEPTED_OTHER_INVITATION")[] | "ACCEPTED" | "INVITED" | "ACCEPTED_OTHER_INVITATION"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ invitation: { id: number, senderProfileId: number, recipientAddress: string, status: "ACCEPTED" | "INVITED" | "ACCEPTED_OTHER_INVITATION", recipientScoreImpact: {…}, senderScoreImpact: {…}, dateInvited: string, dateAccepted: string | null }, invitedUser: {…} | null })[], total: number, limit: number, offset: number }

### GET /invitations/check
Check if a profile is allowed to invite an address or ENS name

Parameters:
- senderProfileId (query, optional): number
- addressOrEns (query, required): string

Response: { canInvite: true, address: string } | { canInvite: false, error: string }

### GET /invitations/pending/{address}
Get pending invitations for an address

Retrieve a list of Ethos profiles that have an active invitation sent to the specified address. Each item includes a simulated score impact that shows how accepting the invitation would affect the recipient score.

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: ({ id: number, impact: { value: number, relativeValue: number, impact: "POSITIVE" | "NEGATIVE" | "NEUTRAL", adjustedRecipientScore: number }, inviterUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null })[]

### GET /invitations/accepted/{senderProfileId}/tree
Get a tree of accepted invitations

Parameters:
- senderProfileId (path, required): number
- depth (query, optional): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, senderProfileId: number, acceptedProfileId: number, level: number, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

## Karma

Karma: season-based points a user earns for daily contributions, prizes and tips, distinct from XP. Read a user’s ledger and totals, the leaderboard and the latest daily prizes.

### GET /karma/daily-prizes
Get the latest daily karma prizes

Response: { day: string | null, prizes: ({ activity: object, amount: number, rank: number, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, weightedUpvotes: number })[] }

### GET /karma/leaderboard
Get the karma leaderboard

Parameters:
- currentUserFirst (query, optional): string | boolean
- direction (query, optional): "next" | "prev"
- lastSeenUserId (query, optional): number
- lastSeenValue (query, optional): number
- pageSize (query, optional): number
- view (query, optional): "season" | "total"

Response: { hasNextPage: boolean, hasPrevPage: boolean, nextCursor?: { direction: "next", lastSeenUserId: number, lastSeenValue: number }, prevCursor?: { direction: "prev", lastSeenUserId: number, lastSeenValue: number }, rankRange: { first: number, last: number } | null, values: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … }, change24h: number, isSelf: boolean, karmaSeason: number, karmaTotal: number, rank: number })[], view: "season" | "total" }

### GET /karma/ledger/{userkey}
Get a user’s karma ledger

Parameters:
- userkey (path, required): string
- types (query, optional): ("XP_CONVERSION" | "DAILY_TASK_REWARD" | "TIP_RECEIVED" | "DAILY_PRIZE" | "DAILY_BOUNTY")[] | "XP_CONVERSION" | "DAILY_TASK_REWARD" | "TIP_RECEIVED" | "DAILY_PRIZE" | "DAILY_BOUNTY"
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ amount: number, createdAt: string, day: string, id: number, seasonId: number, reviewId: …, type: "DAILY_BOUNTY" } | { amount: number, createdAt: string, day: string, id: number, seasonId: number, activity: {…} | null, rank: …, type: "DAILY_PRIZE", weightedUpvotes: number | null } | { amount: number, createdAt: string, day: string, id: number, seasonId: number, payout: …, type: "DAILY_TASK_REWARD" } | { amount: number, createdAt: string, day: string, id: number, seasonId: number, activity: {…} | null, sender: …, type: "TIP_RECEIVED" } | { amount: number, createdAt: string, day: string, id: number, seasonId: number, type: "XP_CONVERSION" })[] }

### GET /karma/me
Get the authenticated user’s karma

Requires authentication.

Response: { dailyTaskReward: number, dailyTaskRewardPaidToday: boolean, eligibility: { canEarn: boolean, humanVerified: boolean, payoutsPaused: boolean }, gapToWinners: number | null, karmaSeason: number, karmaTotal: number, multipliers: { credibility: number, streak: number, streakDays: number, validator: number, validatorCount: number }, seasonChange24h: number, seasonRank: number | null }

### POST /karma/tip
Tip karma to the author of an activity

Requires authentication.

Body: { activityId: number, activityType: "review" | "slash" | "vouch" | "vouch-v2" }

Response: { activityTipCount: number, allowanceRemaining: number }

### GET /karma/tips
List karma tips, newest first

Parameters:
- activityType (query, optional): ("review" | "slash" | "vouch" | "vouch-v2")[] | "review" | "slash" | "vouch" | "vouch-v2"
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ amount: number, createdAt: string, id: number, recipient: {…} | null, sender: {…} | null, tippedActivity: { id: number, link: string, type: "review" | "slash" | "vouch" | "vouch-v2" } })[] }

### GET /karma/tip-allowance
Get the authenticated user’s remaining daily karma tip allowance

Requires authentication.

Response: { amount: number, dailyAllowance: number, isOpen: boolean, isPaused: boolean, remaining: number, resetsAt: string }

## Llm

LLM-backed utilities: translation, content quality checks and user activity summaries.

### POST /llm/translate
Get llm translation

Body: { activityType: "broker-post" | "reply" | "review" | "slash" | "vouch" | "vouch-v2", activityId: number, desiredLanguage: string }

Response: { translatedContent: string, translatedDescription: string, translatedLanguage: "unknown" | string, detectedLanguage?: "unknown" | string, error?: string | null }

### POST /llm/quality
Check the quality of a content

Requires authentication.

Body: { title: string, description?: string }

Response: { score: number, error?: string }

### GET /llm/activity-summary
Get cached activity summary for a user

Parameters:
- userkey (query, required): string

Response: { activityCount: number, generatedAt: string | null, summary: string | null, topAuthors: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } })[], uniqueAuthorsCount: number }

## Markets

Reputation markets (v1) on ethos.markets: market listings, holders, price history and trade simulations.

### GET /markets
List markets with pagination, sorting, and filtering

Parameters:
- orderBy (query, optional): "marketCapWei" | "volumeTotalWei" | "volume24hWei" | "trustRatio" | "distrustRatio" | "score" | "createdAt" | "priceChange24hPercent" | "marketCapChange24hPercent" | "scoreDifferential"
- orderDirection (query, optional): "asc" | "desc"
- dayRange (query, optional): number | null
- filterQuery (query, optional): string
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, creatorAddress: string, positivePrice: string, negativePrice: string, trustVotes: number, distrustVotes: number, createdAt: string, updatedAt: string, basePrice: string, creationCost: string, liquidity: string, configType: "VOLATILE" | "NORMAL" | "INSULATED", user?: {…} | null, stats: { marketCapWei: string, marketCapChange24hWei: string, marketCapChange24hPercent: number, volumeTotalWei: string, volume24hWei: string, priceChange24hPercent: number } })[], total: number, limit: number, offset: number }

### GET /markets/{profileId}/holders
Get market holders with user data

Parameters:
- profileId (path, required): number
- dedupeByUser (query, optional): boolean
- voteType (query, optional): "trust" | "distrust"
- orderBy (query, optional): "holdings" | "date"
- orderDirection (query, optional): "asc" | "desc"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ actorAddress: string, marketId: number, voteType: "trust" | "distrust", total: number, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### GET /markets/{profileId}/price/history
Get market price history for chart with advanced features

Parameters:
- profileId (path, required): number
- timeWindow (query, optional): "1H" | "6H" | "1D" | "7D" | "1M" | "3M" | "1Y" | "All"
- customBucket (query, optional): "30 seconds" | "1 minute" | "5 minutes" | "15 minutes" | "30 minutes" | "1 hour" | "2 hours" | "6 hours" | "12 hours" | "1 day"

Response: { data: { timeBucket: string, price: { trustWei: number, distrustWei: number, trustUsd: number, distrustUsd: number, totalWei: number, totalUsd: number }, marketCap: { trustWei: number, distrustWei: number, trustUsd: number, distrustUsd: number, totalWei: number, totalUsd: number } }[], timeWindow: "1H" | "6H" | "1D" | "7D" | "1M" | "3M" | "1Y" | "All", bucket: "30 seconds" | "1 minute" | "5 minutes" | "15 minutes" | "30 minutes" | "1 hour" | "2 hours" | "6 hours" | "12 hours" | "1 day" }

### GET /markets/simulate-sell
Simulate market vote sell

Parameters:
- profileId (query, required): number
- voteType (query, required): "trust" | "distrust"
- votes (query, required): number
- sellerAddress (query, required): string — Ethereum address (0x-prefixed, 20 bytes)
- slippagePercentage (query, optional): number

Response: { fundsReceived: number, minimumVotePrice: number, newVotePrice: number }

### POST /markets/{profileId}/info
Get enhanced market info with optional includes

Enhanced market info with optional top holders and market change data

Parameters:
- profileId (path, required): number

Body: { marketTopHoldersLimit?: number, marketChangeParams?: { type?: "marketCap" | "price", days?: number } }

Response: { id: number, creatorAddress: string, positivePrice: string, negativePrice: string, trustVotes: number, distrustVotes: number, createdAt: string, updatedAt: string, basePrice: string, creationCost: string, liquidity: string, configType: "VOLATILE" | "NORMAL" | "INSULATED", user?: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } } | null, stats: { marketCapWei: string, marketCapChange24hWei: string, marketCapChange24hPercent: number, volumeTotalWei: string, volume24hWei: string, priceChange24hPercent: number }, marketTopHolders?: { trust: ({ actorAddress: string, marketId: number, voteType: "trust" | "distrust", total: number, user: {…} })[], distrust: ({ actorAddress: string, marketId: number, voteType: "trust" | "distrust", total: number, user: {…} })[] }, marketChange?: { profileId: number, change: { trust: { latestWei: number, latestUsd: number, changeWei: …, changeUsd: number | null, changePercent: number | null }, distrust: { latestWei: number, latestUsd: number, changeWei: …, changeUsd: number | null, changePercent: number | null }, total: { latestWei: number, latestUsd: number, changeWei: …, changeUsd: number | null, changePercent: number | null } }, days: number } }

### GET /markets/info/bulk
Get market info for multiple markets by profile IDs

Parameters:
- profileIds (query, required): number[]

Response: ({ id: number, creatorAddress: string, positivePrice: string, negativePrice: string, trustVotes: number, distrustVotes: number, createdAt: string, updatedAt: string, basePrice: string, creationCost: string, liquidity: string, configType: "VOLATILE" | "NORMAL" | "INSULATED", user?: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null, stats: { marketCapWei: string, marketCapChange24hWei: string, marketCapChange24hPercent: number, volumeTotalWei: string, volume24hWei: string, priceChange24hPercent: number } })[]

### GET /markets/users/by/address/{address}
Get markets user by the embedded wallet address used on ethos.markets

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { profileId: number | null, twitterUsername: string, twitterName: string, twitterUserId: string, walletAddress: string | null, avatarUrl: string, createdAt: string, updatedAt: string }

### GET /markets/{profileId}/user-votes
Get vote counts owned by a user in a market

Parameters:
- profileId (path, required): number
- userAddress (query, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { distrustVotes: number, trustVotes: number }

### GET /markets/withdrawable
Get what the caller can still withdraw from legacy markets

Open legacy market holdings in the caller’s Ethos Everywhere wallet and ethos.markets wallet, one row per wallet, market and vote type, plus unclaimed market rewards when the caller owns a legacy market.

Requires authentication.

Response: { holdings: ({ marketProfileId: number, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, valueWei: number, voteType: "trust" | "distrust", votes: number, walletAddress: string, walletKind: "eew" | "ethosMarkets" })[], marketRewards: { amountWei: number, recipient: string } | null }

## Markets V2

Reputation markets v2: markets keyed by on-chain id, trade and price history, holders, positions and PNL leaderboards.

### GET /markets-v2
List markets v2 with pagination and sorting

Parameters:
- orderBy (query, optional): "createdAt" | "distance" | "poolBacking" | "priceChange24hPercent" | "trustPrice" | "volume24hCredits" | "volumeTotalCredits"
- orderDirection (query, optional): "asc" | "desc"
- filterQuery (query, optional): string
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ id: number, marketOnchainId: number, userkey: string | null, subject: {…} | null, creatorAddress: string, trustPrice: number, distrustPrice: number, trustSupply: number, distrustSupply: number, poolBacking: number, volumeTotalCredits: number, volume24hCredits: number, priceChange24hPercent: number, createdAt: string })[] }

### GET /markets-v2/held-markets
List markets v2 a user currently holds, ranked by position value desc

Parameters:
- holderUserkey (query, required): string
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ id: number, marketOnchainId: number, userkey: string | null, subject: {…} | null, creatorAddress: string, trustPrice: number, distrustPrice: number, trustSupply: number, distrustSupply: number, poolBacking: number, volumeTotalCredits: number, volume24hCredits: number, priceChange24hPercent: number, createdAt: string })[] }

### GET /markets-v2/for-subject
Get the markets v2 market whose subject is the given user, or null

Parameters:
- userkey (query, required): string

Response: { id: number, marketOnchainId: number, userkey: string | null, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null, creatorAddress: string, trustPrice: number, distrustPrice: number, trustSupply: number, distrustSupply: number, poolBacking: number, volumeTotalCredits: number, volume24hCredits: number, priceChange24hPercent: number, createdAt: string, topTrustHolders: { user: {…}, balance: number }[] } | null

### GET /markets-v2/recent-trades
Get globally-recent trades across all markets v2

Parameters:
- limit (query, optional): number

Response: { values: ({ id: number, type: "BUY" | "SELL", traderAddress: string, trader: {…} | null, isPositive: boolean, paymentAmount: number, tokenAmount: number, createdAt: string, market: { marketOnchainId: number, userkey: string | null, subject: …, priceChange24hPercent: number } })[] }

### GET /markets-v2/all-trade-history
Cursor-paginated trades across all markets v2, with optional score/size filters

Parameters:
- minScore (query, optional): number
- minPaymentAmount (query, optional): string
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ id: number, type: "BUY" | "SELL", traderAddress: string, trader: {…} | null, isPositive: boolean, paymentAmount: number, tokenAmount: number, createdAt: string, market: { marketOnchainId: number, userkey: string | null, subject: … } })[] }

### GET /markets-v2/pnl-leaderboard
Rank traders by realized or unrealized PNL over a rolling window or all-time

Parameters:
- direction (query, required): "winners" | "losers"
- limit (query, optional): number
- metric (query, optional): "realized" | "unrealized"
- window (query, optional): "1d" | "1w" | "1m" | "all"

Response: { metric: "realized" | "unrealized", values: ({ pnl: number, pnlPercent: number, rank: number, tradesCount: number, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[] }

### GET /markets-v2/portfolio-stats
Aggregate portfolio value and unrealized PNL for a user

Parameters:
- holderUserkey (query, required): string

Response: { portfolioValue: number, unrealizedPnl: number }

### GET /markets-v2/open-positions
List every open position a user holds across markets v2, valued at the sell quote

Parameters:
- holderUserkey (query, required): string

Response: { values: ({ market: { id: number, marketOnchainId: number, userkey: string | null, subject: …, creatorAddress: string, trustPrice: number, distrustPrice: number, trustSupply: number, distrustSupply: number, poolBacking: number, volumeTotalCredits: number, volume24hCredits: number, priceChange24hPercent: number, createdAt: string }, isPositive: boolean, value: number, pnl: number, pnlPercent: number | null })[] }

### GET /markets-v2/{marketOnchainId}
Get a single market v2 by on-chain ID

Parameters:
- marketOnchainId (path, required): number

Response: { id: number, marketOnchainId: number, userkey: string | null, subject: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } | null, creatorAddress: string, trustPrice: number, distrustPrice: number, trustSupply: number, distrustSupply: number, poolBacking: number, volumeTotalCredits: number, volume24hCredits: number, priceChange24hPercent: number, createdAt: string, pricingContract: string, trustToken: string, distrustToken: string } | null

### GET /markets-v2/{marketOnchainId}/trade-history
Get trade history for a market v2

Parameters:
- marketOnchainId (path, required): number
- traderAddress (query, optional): string
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ id: number, type: "BUY" | "SELL", traderAddress: string, trader: {…} | null, isPositive: boolean, paymentAmount: number, tokenAmount: number, createdAt: string })[] }

### GET /markets-v2/{marketOnchainId}/price-history
Get continuous per-side price candlestick history for a market v2

Parameters:
- marketOnchainId (path, required): number
- duration (query, optional): "1m" | "5m" | "15m" | "1h" | "1d" | "1w" | "1mo" | "1y"
- side (query, optional): "trust" | "distrust"
- from (query, optional): string
- to (query, optional): string

Response: { duration: "1m" | "5m" | "15m" | "1h" | "1d" | "1w" | "1mo" | "1y", metric: "price", side: "trust" | "distrust", values: { timeBucket: string, open: number, high: number, low: number, close: number }[] }

### GET /markets-v2/{marketOnchainId}/recent-news
Get recent news posted by a market’s subject

Parameters:
- marketOnchainId (path, required): number

Response: { items: { createdAt: string, id: string, text: string, url: string }[] }

### GET /markets-v2/{marketOnchainId}/holders
List holders of a market v2, sorted by balance desc

Parameters:
- marketOnchainId (path, required): number
- side (query, optional): "trust" | "distrust"
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ traderAddress: string, trader: {…} | null, isPositive: boolean, balance: number, firstTradeAt: string })[] }

### GET /markets-v2/{marketOnchainId}/holder-count
Count open holder positions in a market v2

Parameters:
- marketOnchainId (path, required): number

Response: { count: number }

### GET /markets-v2/{marketOnchainId}/position
Get a user’s position in a market

Parameters:
- marketOnchainId (path, required): number
- isPositive (query, required): boolean
- userAddress (query, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { balance: number }

### GET /markets-v2/{marketOnchainId}/position-values
Get a user’s open positions in a market, valued at the sell quote

Parameters:
- marketOnchainId (path, required): number
- holderUserkey (query, required): string

Response: { values: { isPositive: boolean, tokenBalance: number, value: number }[] }

### GET /markets-v2/{marketOnchainId}/simulate-open-position
Simulate opening a position on a market v2

Parameters:
- marketOnchainId (path, required): number
- isPositive (query, required): boolean
- paymentAmount (query, required): number
- slippagePercentage (query, optional): number

Response: { tokensMinted: number, minTokensOut: number, protocolFee: number, effectivePrice: number, trustPriceBefore: number, trustPriceAfter: number, priceImpactPct: number }

### GET /markets-v2/{marketOnchainId}/simulate-close-position
Simulate closing a position on a market v2

Parameters:
- marketOnchainId (path, required): number
- isPositive (query, required): boolean
- positionTokenAmount (query, required): number
- sellerAddress (query, required): string — Ethereum address (0x-prefixed, 20 bytes)
- slippagePercentage (query, optional): number

Response: { netCreditsOut: number, minCreditsOut: number, curveRevenue: number, exitFee: number, effectivePrice: number, trustPriceBefore: number, trustPriceAfter: number, priceImpactPct: number }

## Notifications

The current user’s in-app notifications and notification settings.

### GET /notifications/me
Get pending notifications for user

Requires authentication.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({…} & { id: number, date: string } & {…})[], total: number, limit: number, offset: number }

### GET /notifications/stats/me
Get notifications stats for user

Requires authentication.

Response: { unreadCount: number }

### POST /notifications/me/mark-as-read
Mark notifications as read for user

Requires authentication.

Response: { read: number }

### POST /notifications/me/push-registrations
Register this browser for push notifications

Registers the browser’s Firebase installation ID (fid) for push notifications to the current profile.

Requires authentication.

Body: { deviceIdentifier: string, fid: string }

Response: { result: "created" | "unchanged" | "updated" }

### GET /notifications/me/settings
Get notification settings for user

Requires authentication.

Response: Record<string, { listDisabled: boolean, pushDisabled: boolean }>

### PUT /notifications/me/settings
Update notification settings for user

Requires authentication.

Body: { settings: Record<string, { listDisabled: boolean, pushDisabled: boolean }> }

Response: unknown

## Nfts

NFTs a user holds from tracked collections, validator and Meow Meows holdings, and validator listings.

### GET /nfts/user/{ethosUserKey}/owns-validator
Check if a user owns a validator NFT

Parameters:
- ethosUserKey (path, required): string

Response: ({ contractAddress: string, tokenId: string, name: string | null, description: string | null, imageUrl: string | null, thumbnailUrl: string | null, animationUrl: string | null, animationMime: string | null, tokenUri: string | null, attributesJson: unknown | null, mintedAt: string | null, acquiredAt: string | null, lastActivityAt: string | null, createdAt: string, updatedAt: string, NftOwners: { ownerAddress: string }[], collection: { slug: string | null } })[]

### GET /nfts/user/{ethosUserKey}/meow-meows
Get the Meow Meows NFTs a user owns

Parameters:
- ethosUserKey (path, required): string

Response: ({ contractAddress: string, tokenId: string, name: string | null, description: string | null, imageUrl: string | null, thumbnailUrl: string | null, animationUrl: string | null, animationMime: string | null, tokenUri: string | null, attributesJson: unknown | null, mintedAt: string | null, acquiredAt: string | null, lastActivityAt: string | null, createdAt: string, updatedAt: string, NftOwners: { ownerAddress: string }[], collection: { slug: string | null } })[]

### GET /nfts/user/{ethosUserKey}
Get NFTs owned by a user

Parameters:
- ethosUserKey (path, required): string
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ contractAddress: string, tokenId: string, name: string | null, description: string | null, imageUrl: string | null, thumbnailUrl: string | null, animationUrl: string | null, animationMime: string | null, tokenUri: string | null, attributesJson: unknown | null, mintedAt: string | null, acquiredAt: string | null, lastActivityAt: string | null, createdAt: string, updatedAt: string, NftOwners: {…}[], collection: { slug: string | null } })[], total: number, limit: number, offset: number }

### GET /nfts/validators/listings
Get validator NFTs listed for sale on OpenSea

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ orderHash: string, tokenId: string, name: string | null, imageUrl: string | null, priceWei: string, priceEth: string, currency: string, seller: string, openseaUrl: string })[], total: number, limit: number, offset: number }

## Profiles

On-chain Ethos profiles. A profile is the on-chain identity created when someone joins; use the users endpoints for the person record and score.

### POST /profiles/addresses
Get profile addresses for multiple userkeys

Returns one entry per requested userkey, in request order. `addresses` lists the addresses registered to the profile, in registration order, each typed as `ETHOS_WALLET` (the Ethos Everywhere wallet) or `EXTERNAL_EVM_WALLET` (a wallet the user brought). Privy smart wallets are not listed. `profileId` is null when an address or service account is not attached to a profile; a `profileId:` userkey always returns that id, with an empty list when the profile has no addresses.

Body: { userkeys: string[] }

Response: ({ addresses: ({ address: string, type: "ETHOS_WALLET" | "EXTERNAL_EVM_WALLET" })[], profileId: number | null, userkey: string })[]

### POST /profiles
Get a paginated list of profiles

Lists on-chain profiles filtered by id or address (up to 500 each), archived state and invite availability, sorted by creation date or invites available. For the person record and score behind a profile, use the users endpoints.

Body: { ids?: number[], addresses?: string[], archived?: boolean, sortField?: "createdAt" | "invitesAvailable", sortDirection?: "asc" | "desc", inviteFilter?: "hasInvites" | "noInvites", limit?: number, offset?: number }

Response: { values: ({ profile: { id: number, archived: boolean, createdAt: number, updatedAt: number, invitesAvailable: number, invitedBy: number }, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, inviterUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### GET /profiles/recent
Get recently created profiles

Returns the newest profiles, each with its linked user and the user who invited them.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ profile: { id: number, archived: boolean, createdAt: number, updatedAt: number, invitesAvailable: number, invitedBy: number }, user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, inviterUser: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} } })[], total: number, limit: number, offset: number }

### GET /profiles/stats
Get profiles stats

Returns the number of active profiles and the total invites available across them.

Response: { activeProfiles: number, invitesAvailable: number }

## Replies

Threaded replies on activities, with pinning and deep-link helpers.

### GET /replies/by-id
Get replies by ids

> ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- ids (query, required): number[]

Response: Record<string, { id: number, contractType: "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket", targetContract: string, parentId: number, authorProfileId: number, createdAt: number, content: string, metadata: string, pinned: boolean, user: {…} | null, url: string } | null>

### GET /replies/{replyId}/deep-link-info
Get ancestor chain and root activity for a reply

Returns the ancestor reply IDs needed to deep link to a specific reply.

Parameters:
- replyId (path, required): number

Response: { ancestorIds: number[] }

### GET /replies/{replyId}/offset
Get the position of a reply in a paginated list

Returns the 0-based offset of a reply within its parent reply list, given sort parameters. Used for deep-linking to navigate to the correct page.

Parameters:
- replyId (path, required): number
- contractType (query, required): "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket"
- excludeUnverified (query, optional): boolean
- orderBy (query, optional): "score" | "createdAt" | "votes"
- orderDirection (query, optional): "asc" | "desc"
- parentId (query, required): number

Response: { offset: number }

### GET /replies/{contractType}/{parentId}
Get replies for a specific activity or another reply

> ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- contractType (path, required): "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket"
- parentId (path, required): number
- excludeUnverified (query, optional): boolean
- orderBy (query, optional): "score" | "createdAt" | "votes"
- orderDirection (query, optional): "asc" | "desc"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, contractType: "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket", targetContract: string, parentId: number, authorProfileId: number, createdAt: number, content: string, metadata: string, pinned: boolean, user: {…} | null })[], total: number, limit: number, offset: number }

### PUT /replies/{replyId}/pin
Pin or unpin a reply

Pin or unpin a reply on slash activity. Only the slasher and defender can pin their own replies. Only one top-level reply can be pinned at a time — pinning a new reply will automatically unpin any previously pinned reply for the same activity.

Requires authentication.

Parameters:
- replyId (path, required): number

Body: { pinned: boolean }

Response: { ok: boolean, unpinnedReplies: number[] }

## Reviews

Reviews are positive, neutral or negative written assessments one user leaves about another. Use activities to list reviews; these endpoints answer questions about an author and subject pair, or resolve a review from its batch id.

### GET /reviews/count/between
Count reviews left from one user to another

Counts the reviews the author has written about the subject. Both are userkeys; use the numeric X id form rather than a handle. To list reviews with their content, use the activities endpoints.

Parameters:
- authorUserKey (query, required): string
- subjectUserKey (query, required): string

Response: number

### GET /reviews/latest/between
Get the latest review from one user to another

Returns the most recent review the author has written about the subject, or null if there is none. Set excludeLinkedToVouch=true to ignore reviews that were attached to a vouch.

Parameters:
- authorUserKey (query, required): string
- subjectUserKey (query, required): string
- excludeLinkedToVouch (query, optional): boolean

Response: object

### GET /reviews/id-by-batch
Get a review id by its batchId

Resolves a batchId to the review it links to, or null. A batchId is a client-generated id that pairs a review with a vouch; unvouching clears it.

Parameters:
- batchId (query, required): string

Response: { reviewId: number | null }

## Score

Credibility scores. A score is a number from 0 to 2800 with a named level. Look up by address, user id or userkey, singly or in bulk, and read score history and recent-update lists.

### GET /score/updates
Get users with score updates since a timestamp

Returns the userkeys whose score changed in the window, for keeping a local copy of scores current without polling every user. Filter with after and optional until; after must be no more than 7 days old and the window may span at most 7 days. Each result carries the userkey and lastUpdatedAt but no score, so follow up with /score/userkeys; the response is offset-paginated.

Parameters:
- after (query, required): string
- until (query, optional): string
- limit (query, optional): number
- offset (query, optional): number

Response: { values: { userkey: string, lastUpdatedAt: string }[], total: number, limit: number, offset: number }

### GET /score/address
Get score by address

Returns the credibility score and level for an Ethereum address. An address Ethos has not scored returns 0 and untrusted; pass triggerCalculation=true to queue a background calculation for it, then poll /score/status.

Parameters:
- address (query, required): string — Ethereum address (0x-prefixed, 20 bytes)
- triggerCalculation (query, optional): boolean

Response: { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }

### POST /score/addresses
Get scores by multiple addresses

Bulk form of /score/address. Returns an object keyed by each address exactly as passed. Unscored addresses return 0 and untrusted; triggerCalculation=true queues a calculation for each of them.

Body: { addresses: string[], triggerCalculation?: boolean }

Response: Record<string, { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }>

### GET /score/userId
Get score by userId

Returns the credibility score and level for an Ethos user id. Unknown ids return 0 and untrusted; no calculation is triggered.

Parameters:
- userId (query, required): number

Response: { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }

### POST /score/userIds
Get scores by multiple userIds

Bulk form of /score/userId. Returns an object keyed by user id. Unknown ids return 0 and untrusted.

Body: { userIds: number[] }

Response: Record<string, { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }>

### GET /score/userkey
Get score by userkey

The most general score lookup. Accepts any userkey, including service:x.com:username:<handle> for an X handle. Returns 404 when an X handle is unknown. Unscored targets return 0 and untrusted; pass triggerCalculation=true to queue a background calculation, then poll /score/status.

Parameters:
- userkey (query, required): string
- triggerCalculation (query, optional): boolean

Response: { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }

### POST /score/userkeys
Get scores by multiple userkeys

Bulk form of /score/userkey. Returns an object keyed by each userkey exactly as passed. X handles are resolved only if Ethos has already seen them; userkeys that cannot be resolved return 0 and untrusted rather than an error.

Body: { userkeys: string[], triggerCalculation?: boolean }

Response: Record<string, { score?: number, level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned" }>

### GET /score/status
Check score calculation status

Reports whether a score calculation is queued or running for a userkey. Poll this after requesting a lookup with triggerCalculation=true. The job is enqueued in the background, so an immediate poll can still say idle. Poll every second or so for a bounded period, such as ten seconds; once status is idle after that window, fetch the score again.

Parameters:
- userkey (query, required): string

Response: { status: "idle" | "queued" | "calculating", isQueued: boolean, isCalculating: boolean, isPending: boolean }

### GET /score/history
Get score history

Returns dated score points for a userkey over the requested duration (1d to all time, default 30d), aggregated for charting.

Parameters:
- userkey (query, required): string
- duration (query, optional): "1d" | "7d" | "30d" | "90d" | "6m" | "1y" | "all"

Response: { date: string, score: number }[]

## Signatures

Server-signed payloads required by some on-chain actions, such as registering an address.

### POST /signatures/register-address
Get signature for registering an address for an Ethos profile

Requires authentication.

Body: { type: "delegateXyz" | "embeddedWallet" | "externalWallet" | "smartWallet", address: string }

Response: { randValue: number, signature: string }

### POST /solana/transfer-proofs
Start proving a Solana wallet by transfer

Returns the address and exact lamport amount to send from the wallet being proven, or the profile’s open request, or a recently verified one that isn’t attested yet.

Requires authentication.

Body: { newWallet?: boolean }

Response: { amountLamports: number, expiresAt: string, id: number, receiver: string, status: "PENDING" } | { id: number, status: "VERIFIED", wallet: string }

### GET /solana/transfer-proofs/current
Get the Solana transfer proof to resume

Returns the profile’s open request, or a recently verified one that isn’t attested yet, without opening a new one. Null when there is neither.

Requires authentication.

Response: { amountLamports: number, expiresAt: string, id: number, receiver: string, status: "PENDING" } | { id: number, status: "VERIFIED", wallet: string } | null

### POST /solana/transfer-proofs/{id}/check
Check a Solana transfer proof

Looks for the requested transfer. Once found, returns a signature for a solana attestation of the sending wallet, which the caller submits to the blockchain.

Requires authentication.

Parameters:
- id (path, required): number

Response: { status: "EXPIRED" | "PENDING" } | { attestation: { account: string, evidence: string, randValue: number, signature: string } | null, status: "VERIFIED", wallet: string }

### GET /solana/linked-apps
Find app accounts that use a proven Solana wallet

Requires authentication.

Parameters:
- wallet (query, required): string

Response: { accounts: ({ account: string, alreadyConnected: boolean, service: "pump.fun", username: string | null })[] }

### POST /solana/linked-apps/signature
Get signature for attesting an app account found from a proven Solana wallet

Requires authentication.

Body: { wallet: string, service: "pump.fun" }

Response: { account: string, evidence: string, randValue: number, signature: string }

## Slash

Slashes: staked accusations against a user. Supporters who vouched for, positively reviewed or verified a slashed user can be penalized.

### GET /slash/supporter-penalties/{profileId}
Get supporter penalties with enriched slash/subject data

Parameters:
- profileId (path, required): number

Response: ({ id: number, slashId: number, penaltyAmount: number, createdAt: string, reason: "vouch" | "review" | "verification", slash: object })[]

## Stats

Network-wide statistics such as score distribution.

### GET /stats/score-distribution
Get score distribution by level

Response: { distribution: ({ level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned", profileCount: number, nonProfileCount: number })[], totals: { totalUsers: number, totalProfiles: number, totalNonProfiles: number } }

### GET /stats/human-verification
Get human verification statistics

Response: { summary: { verifiedCount: number, qualifiedVerifierCount: number, pendingCount: number }, progressDistribution: { bonds: number, userCount: number }[], verifiedByScoreLevel: ({ level: "untrusted" | "questionable" | "neutral" | "known" | "established" | "reputable" | "exemplary" | "distinguished" | "revered" | "renowned", count: number })[], methodBreakdown: ({ method: "IRL" | "VIDEO" | "VOICE", count: number })[] }

## System

Health checks and transaction processing status.

### GET /healthcheck
Health check endpoint

Response: { ok: boolean }

### GET /healthcheck/authenticated
Authenticated health check endpoint

Requires authentication.

Response: { ok: boolean, profileId?: number }

### GET /healthcheck/always-fail
Always fail health check endpoint with 418 teapot

Response: unknown

### GET /transactions/{txHash}/processed
Wait until a blockchain transaction has been processed

Waits until the transaction has been processed by Ethos event processors or until the timeout elapses. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- txHash (path, required): string
- timeoutMs (query, optional): number

Response: { processed: boolean, status: "not_found" | "pending" | "processing" | "processed" }

## Users

The aggregated person record Ethos scores. Look a user up by address, username, profile id, or X, Discord, Farcaster or Telegram account, in bulk or singly, or search by name.

### POST /users/by/ids
Get multiple users by user IDs

Bulk lookup by Ethos user id, up to 500 per request. Unknown ids are omitted from the result rather than causing an error.

Body: { userIds: number[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### POST /users/by/address
Get multiple users by Ethereum addresses

Bulk lookup by Ethereum address, up to 500 per request. Addresses with no Ethos user are omitted. If you only need scores, /score/addresses is cheaper.

Body: { addresses: string[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### POST /users/by/profile-id
Get multiple users by profile IDs

Bulk lookup by on-chain profile id, up to 500 per request. Profiles without a user record are omitted.

Body: { profileIds: number[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### POST /users/by/x
Get multiple users by Twitter/X account IDs or usernames

Bulk lookup by X (Twitter) numeric account id or handle, mixed freely, up to 500 per request. Accounts Ethos does not know are omitted.

Body: { accountIdsOrUsernames: string[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### POST /users/by/discord
Get multiple users by Discord user IDs

Bulk lookup by Discord user id, up to 500 per request. Ids with no linked Ethos user are omitted.

Body: { discordIds: string[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### POST /users/by/telegram
Get multiple users by Telegram user IDs

Bulk lookup by Telegram user id, up to 500 per request. Ids with no linked Ethos user are omitted.

Body: { telegramIds: string[] }

Response: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: {…} }, vouch: { given: {…}, received: {…} }, vouchTotals: {…}[], whufGuarantee: {…} | null } })[]

### GET /users/search
Search users by query string

Searches by display name, Ethos username, ENS name or exact address. Use an exact lookup endpoint when you already know an address or account id. Returns 429 when search is busy or the query is too slow; retry with a more specific query.

Parameters:
- query (query, required): string
- userKeyType (query, optional): "ADDRESS" | "DISCORD" | "FARCASTER" | "GITHUB" | "PUMPFUN" | "SOLANA" | "TELEGRAM" | "TWITTER" | "PROFILE"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } })[], total: number, limit: number, offset: number }

### GET /user/{userId}
Get a specific user by user ID

Returns one user by Ethos user id, including score, XP, status and every linked userkey. Returns 404 if the id is unknown.

Parameters:
- userId (path, required): number

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/address/{address}
Get a user by Ethereum address

Returns the user linked to an Ethereum address, or 404 if Ethos has no record of it. For the score alone, /score/address never 404s and can trigger a calculation.

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/ethos-everywhere-wallet/{address}
Get a user by Ethos Everywhere wallet address

Resolves an Ethos Everywhere wallet (the embedded wallet Ethos creates for a user) to its user. For a wallet the user connected themselves, use /user/by/address.

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/profile-id/{profileId}
Get a user by profile ID

Returns the user behind an on-chain profile id, or 404 if none. A profile id is what on-chain events reference.

Parameters:
- profileId (path, required): number

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/username/{username}
Get a user by username

Returns a user by Ethos username, the handle shown in app.ethos.network profile URLs. Returns 404 if none.

Parameters:
- username (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/x/{accountIdOrUsername}
Get a user by Twitter/X account ID or username

Returns the user linked to an X (Twitter) account, by numeric id or handle. The usual entry point for social integrations. Returns 404 if the account is not known to Ethos; the score endpoints can still score it.

Parameters:
- accountIdOrUsername (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/discord/{discordUserId}
Get a user by Discord user ID

Returns the user who linked this Discord account, or 404 if none.

Parameters:
- discordUserId (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/farcaster/{farcasterUserId}
[Deprecated] Get a user by Farcaster user ID.

Returns the user who linked this Farcaster fid, or 404 if none.

Parameters:
- farcasterUserId (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/farcaster/username/{farcasterUsername}
[Deprecated] Get a user by Farcaster username.

Resolves a Farcaster username to its fid and returns the linked user. Returns 404 if the username has no Ethos user.

Parameters:
- farcasterUsername (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /user/by/telegram/{telegramUserId}
Get a user by Telegram user ID

Returns the user who linked this Telegram account, or 404 if none.

Parameters:
- telegramUserId (path, required): string

Response: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: number | null, review: { received: { negative: number, neutral: number, positive: number } }, vouch: { given: { amountWeiTotal: number, count: number }, received: { amountWeiTotal: number, count: number } }, vouchTotals: { balance: {…}, count: {…}, token: {…} }[], whufGuarantee: { asOf: string, expiresAt: string | null, guaranteedWei: number, status: "active" | "expired" } | null } }

### GET /users/{userkey}/categories
Get position in each category for user

Returns the user’s leaderboard rank within each category they belong to. Returns 404 if the userkey is unknown.

Parameters:
- userkey (path, required): string

Response: { categoryRanks: { rank: number, category: { id: number, slug?: string, name: string, description?: string, showOnLeaderboard: boolean, showInDailyService: boolean, showInDailyBounty: boolean, bannerImageUrl?: string, userCount: number } }[] }

### POST /users/refresh/twitter
Refresh x.com profile details like name, avatar, etc.

Queues a refresh of the signed-in user’s X profile details. Throttled to one refresh per profile every 60 minutes; a throttled call returns ok=false.

Requires authentication.

Response: { ok: boolean, message: string }

### GET /users/me/review-suggestions
Get suggested Ethos users for the current user to review

Requires a session. Returns users the signed-in user is likely to know and has not reviewed yet. Empty unless the user has a linked X account and an established score.

Requires authentication.

Parameters:
- limit (query, optional): number

Response: { values: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } })[] }

### GET /users/me/discover-suggestions
Get suggested users to review, endorsed by your connections

Requires a session. Returns users endorsed by the signed-in user’s connections or followed on X, each with a sample of the connections endorsing them.

Requires authentication.

Parameters:
- endorserSampleSize (query, optional): number
- limit (query, optional): number

Response: { values: ({ user: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, endorsers: {…}[], endorserCount: number, connectionDegree: "1st" | "2nd" | "3rd-author" | "3rd-subject" | "4th-author" | "4th-subject" | "none", source: "connections" | "twitter" })[] }

### GET /users/{userkey}/top-quality-wallet
Get the user’s top-scoring wallet address by wallet quality

Returns the linked wallet with the highest wallet-quality score, or null if the user has no scored wallet.

Parameters:
- userkey (path, required): string

Response: { address: string | null }

Also in this group:
- GET /internal/users/embedded-wallet/{address} — Get user by embedded wallet (documented under Internal)
- GET /internal/users/{userkey} — Get user by userkey (documented under Internal)

## Votes

Upvotes and downvotes on activities, and aggregated vote stats.

### GET /votes
Get votes for an activity

Parameters:
- type (query, required): "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket"
- activityId (query, required): number
- isUpvote (query, optional): "true" | "false"
- orderBy (query, optional): "score" | "updatedAt"
- orderDirection (query, optional): "asc" | "desc"
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ isUpvote: boolean, isArchived: boolean, voter: number, targetContract: string, targetId: number, createdAt: number, weight: number, user?: {…} | null })[], total: number, limit: number, offset: number }

### GET /votes/stats
Get aggregated vote stats for a single activity

Parameters:
- type (query, required): "attestation" | "bond" | "broker" | "discussion" | "review" | "slash" | "vouch" | "vouchV2" | "project" | "reputationMarket"
- activityId (query, required): number
- includeArchived (query, optional): boolean

Response: { userVote: { isUpvote: boolean, isArchived: boolean, voter: number, targetContract: string, targetId: number, createdAt: number, weight: number, user?: {…} | null } | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }

### POST /votes/stats
Get aggregated vote stats for multiple activities

Body: { attestation?: number[], bond?: number[], discussion?: number[], review?: number[], slash?: number[], vouch?: number[], vouchV2?: number[], project?: number[], reputationMarket?: number[], broker?: number[], includeArchived?: boolean }

Response: { attestation: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, bond: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, discussion: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, review: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, slash: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, vouch: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, vouchV2: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, project: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, reputationMarket: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }>, broker: Record<string, { userVote: {…} | null, counts: { upvotes: number, downvotes: number }, weights: { weightedUpvotes: number, weightedDownvotes: number, upvotePercentage?: number, downvotePercentage?: number } }> }

## Vouches

Vouches are stakes one profile places on another as a signal of trust. Query vouches with filters, or check pair state, mutual vouchers and fees.

### POST /vouches
Query vouches with optional filters

> ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Body: { ids?: number[], subjectUserkeys?: string[], authorProfileIds?: number[], subjectProfileIds?: number[], subjectAddresses?: string[], subjectAttestationHashes?: string[], archived?: boolean, afterDate?: string, orderBy?: { balance?: "asc" | "desc", vouchedAt?: "asc" | "desc", updatedAt?: "asc" | "desc" }, duration?: string, limit?: number, offset?: number }

Response: { values: ({ authorProfileId: number, subjectProfileId: number, authorUser?: {…} | null, subjectUser?: {…} | null })[], total: number, limit: number, offset: number }

### GET /vouches/authored-subject-userkeys
Get every subject userkey a profile actively vouches for

Unpaginated, and version-erased — one set, never one per version — bounded by how many vouches one person has authored. Each subject is expanded to all of its userkeys, so a caller can test membership against a user’s full key list. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- profileId (query, required): number

Response: { userkeys: unknown[] }

### GET /vouches/authored-details
Get a profile’s active WHUF vouches with their manage-time details

Unpaginated, and WHUF vouches only — never ETH-staked vouches. Carries the staked amount, author, and the review each vouch was composed with, plus every userkey of each subject so a caller can look a vouch up by whichever identity it holds. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- profileId (query, required): number

Response: { vouches: { authorAddress: string, balanceWei: string, pairedReview: {…}, subjectUserkey: unknown, subjectUserkeys: unknown[], vouchId: number }[] }

### GET /vouches/incoming-author-profile-ids
Get the profiles holding an active vouch toward a profile

Unpaginated, and version-erased — one set of authors, never one per version. Authors without an Ethos profile are omitted. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- profileId (query, required): number

Response: { authorProfileIds: number[] }

### GET /vouches/pair-state
Get the active vouch the caller already holds on a subject

Answers for the pair, not for one encoding: every wallet the caller holds is checked against every userkey of the subject, across both vouch versions. The author is always the authenticated caller, so the answer only ever describes the caller’s own vouch. Use it before offering to vouch — a non-`none` answer means the V2 write paths that run the pair rule would refuse a new vouch, and the existing vouch is the one to increase or convert. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- subjectUserkey (query, required): string

Response: { vouchId: number, kind: "v1" } | { vouchId: number, kind: "v2" } | { kind: "none" }

### GET /vouches/protected/exists
Check whether the caller has any protected vouches

Whether any of your vouches were frozen when WHUF started. Cheaper than listing them: answers from the snapshot alone, without resolving each vouch. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- profileId (query, required): number

Response: { hasProtectedVouches: boolean }

### GET /vouches/protected
Get the caller’s protected vouches

List the vouches that you made before WHUF started. You have not converted these vouches to WHUF yet. Cursor-paginated, newest vouch id first. Each item carries two amounts: `stakedWei` is the amount frozen at the snapshot, and `liveBalanceWei` is the current V1 balance. You can convert the vouch to WHUF, or vouch again if you already removed it. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- profileId (query, required): number
- cursor (query, optional): number
- limit (query, optional): number

Response: { nextCursor: number | null, values: ({ authorAddress: string, liveBalanceWei: string, state: "live" | "archived", stakedWei: string, subjectUser: {…} | null, subjectUserkey: unknown, vouchId: number, vouchedAt: number })[], total: number }

### GET /vouches/mutual-vouchers
Get mutual vouchers for a viewer and target profile

Users the viewer vouches for who also vouch for the target. Scoped to WHUF vouches. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Parameters:
- viewerProfileId (query, required): number
- targetProfileId (query, required): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } })[], total: number, limit: number, offset: number }

### GET /vouches/fees
Get vouch creation fees

Response: { entryDonationFeeBasisPoints: number, entryProtocolFeeBasisPoints: number, entryVouchersPoolFeeBasisPoints: number, exitFeeBasisPoints: number }

### GET /vouches/v2/fees
Get V2 vouch entry/exit fee bps

Linear with amount: `fee = amount * entryFeeBps / 10000`. Also surfaces the immutable payment token address and its EIP-712 domain so frontends can build permits without reading those from chain. Amount-dependent fees (previewVouchFee, reviewPrice) are intentionally NOT cached — read them live. Cached for 12h. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Response: { entryFeeBps: number, exitFeeBps: number, minimumVouchAmount: number, maximumVouchAmount: number, paymentToken: string, paymentTokenDomain: { name: string, version: string, chainId: number, verifyingContract: string } }

## Wallets

On-chain actions signed by the user’s Ethos Everywhere wallet: reviews, votes, replies, invites, vouches, humanity bonds, market trades and WHUF sends, plus the wallet’s transaction list. First-party browser sessions only: needs the session cookies from /auth/exchange and an Ethos origin.

### POST /auth/exchange
Exchange Privy session for Ethos JWTs

Verifies the Privy bearer token and sets Ethos access/refresh JWTs via HttpOnly cookies.

Requires authentication.

Response: { ok: true }

### POST /auth/logout
Logout and clear Ethos JWT cookies

Clears Ethos access/refresh cookies and revokes the current refresh token if present. If fid or deviceIdentifier is provided, also removes the push registration for that browser. Safe to call even if not authenticated.

Body: { deviceIdentifier?: string, fid?: string }

Response: { ok: true }

### GET /wallets/privy/auth-check
Verify authentication for Ethos Everywhere wallet

Returns ok when authenticated. If the access token is expired, it will be refreshed using the refresh token.

Requires authentication.

Response: { ok: boolean, profileId: number }

### GET /wallets/privy/check-funds
Check if wallet has enough funds for transactions

Returns whether the Ethos Everywhere wallet has sufficient balance for transactions like reviews, votes, or replies. Also indicates if gas sponsorship is available.

Requires authentication.

Response: { balance: number, sponsorshipEligible: boolean }

### POST /wallets/privy/post/review
Create a review via an Ethos Everywhere wallet

Signs and submits a review transaction using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { score: "negative" | "neutral" | "positive", title: string, content?: string, metadata?: ({ key: string, value?: string | number | boolean })[] | Record<string, string | number | boolean>, subject: { address: string } | { service: string, account: string, accountType?: "username" | "id" } | { x: {…} }, waitForTxTimeoutSeconds?: number }

Response: { hash: string, review?: { id: number, authorProfileId: number, author: string, subject: string, score: "negative" | "neutral" | "positive", comment: string, metadata: string, createdAt: number, archived: boolean, attestationDetails?: { account: string, service: string } }, reviewSlug?: string }

### POST /wallets/privy/post/review/by-address
Create a review by address via an Ethos Everywhere wallet

Signs and submits a review transaction for a subject address using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { score: "negative" | "neutral" | "positive", title: string, content?: string, metadata?: ({ key: string, value?: string | number | boolean })[] | Record<string, string | number | boolean>, address: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, review?: { id: number, authorProfileId: number, author: string, subject: string, score: "negative" | "neutral" | "positive", comment: string, metadata: string, createdAt: number, archived: boolean, attestationDetails?: { account: string, service: string } }, reviewSlug?: string }

### POST /wallets/privy/post/review/by-x
Create a review for x.com account via an Ethos Everywhere wallet

Signs and submits a review transaction for an x.com account (id or username) using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { score: "negative" | "neutral" | "positive", title: string, content?: string, metadata?: ({ key: string, value?: string | number | boolean })[] | Record<string, string | number | boolean>, x: { id: string } | { username: string }, waitForTxTimeoutSeconds?: number }

Response: { hash: string, review?: { id: number, authorProfileId: number, author: string, subject: string, score: "negative" | "neutral" | "positive", comment: string, metadata: string, createdAt: number, archived: boolean, attestationDetails?: { account: string, service: string } }, reviewSlug?: string }

### POST /wallets/privy/edit/review
Edit a review via an Ethos Everywhere wallet

Signs and submits a review edit transaction using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { id: number, title: string, content?: string, metadata?: ({ key: string, value?: string | number | boolean })[] | Record<string, string | number | boolean>, waitForTxTimeoutSeconds?: number }

Response: { hash: string, review?: { id: number, authorProfileId: number, author: string, subject: string, score: "negative" | "neutral" | "positive", comment: string, metadata: string, createdAt: number, archived: boolean, attestationDetails?: { account: string, service: string } }, reviewSlug?: string }

### POST /wallets/activity/{activityType}/{id}/vote
Vote on an activity via Ethos Everywhere wallet

Submits an upvote or downvote on a review, vouch, attestation, slash, broker post, project, reputation market, or reply using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- activityType (path, required): "attestation" | "broker-post" | "closed-slash" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "open-slash" | "project" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "vote" | "xp-tip"
- id (path, required): number

Body: { isUpvote: boolean, isArchive?: boolean, waitForTxTimeoutSeconds?: number }

Response: { hash: string, vote?: object }

### POST /wallets/activity/{activityType}/{id}/reply
Reply to an activity via Ethos Everywhere wallet

Posts a reply to a review, vouch, attestation, slash, broker post, project, reputation market, or existing reply (use activityType='reply') using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- activityType (path, required): "attestation" | "broker-post" | "closed-slash" | "human-verification" | "invitation-accepted" | "karma-tip" | "market" | "market-v2-trade" | "market-vote" | "open-slash" | "project" | "reply" | "review" | "review-archived" | "slash" | "unvouch" | "unvouch-v2" | "vouch" | "vouch-v2" | "vouch-v2-decrease" | "vouch-v2-increase" | "vote" | "xp-tip"
- id (path, required): number

Body: { content: string, metadata?: ({ key: string, value?: string | number | boolean })[] | Record<string, string | number | boolean>, waitForTxTimeoutSeconds?: number }

Response: { hash: string, reply?: object }

### POST /wallets/activity/review/{id}/archive
Archive a review via Ethos Everywhere wallet

Archives a review using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- id (path, required): number

Body: { waitForTxTimeoutSeconds?: number }

Response: { hash: string, review?: object }

### POST /wallets/privy/invite/address
Invite an address via Ethos Everywhere wallet

Invites an address to Ethos using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { invitee: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/invite/cancel
Cancel an invite via Ethos Everywhere wallet

Cancels an invitation (uninvites a user) using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { invitee: string, waitForUninviteTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/activity/bond/{bondId}/archive
Archive a bond via Ethos Everywhere wallet

Archives a bond using the user's Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- bondId (path, required): number

Body: { archiveReason: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string }

### POST /wallets/privy/bonds
Create a bond via Ethos Everywhere wallet

Creates a bond using the user's Ethos Everywhere wallet. The signature must be obtained from the signatures.createHumanityBond endpoint first. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { statement: string, bondType: string, expiration: string, amount: string, amountType: string, metadata?: string, randValue: number, signature: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/vouches
Vouch for a user via Ethos Everywhere wallet

Submits a vouch transaction using the user’s Ethos Everywhere wallet. The user’s EEW wallet must hold sufficient ETH to cover both the stake amount and gas.

Requires authentication.

Body: { target: { address: string } | { profileId: number } | { attestationHash: string }, comment: string, metadata?: { description?: string, source?: string, importedFromTestnet?: number, batchId?: string, reviewId?: number, convertedFromVouchV1Id?: number, client?: string, clientSource?: string, version?: string }, paymentAmountEth: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/vouches/unvouch
Unvouch via Ethos Everywhere wallet

Submits an unvouch transaction using the user’s Ethos Everywhere wallet. Restricted to vouches whose original authorAddress equals the EEW. The contract refunds `balance - exitFee` to the original authorAddress (the EEW). When `isHealthy=false`, dispatches to `unvouchUnhealthy` (combo unvouch + markUnhealthy).

Requires authentication.

Body: { vouchId: number, isHealthy: boolean, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/markets/sell
Sell market votes via Ethos Everywhere wallet

Submits a sellVotes transaction on the ReputationMarket contract using the user’s Ethos Everywhere wallet. The contract sends ETH proceeds back to the seller.

Requires authentication.

Body: { profileId: number, isPositive: boolean, amount: number, minimumVotePrice: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/markets-v2/open-position
Open a position on a v2 market via Ethos Everywhere wallet

Submits an openPosition transaction on the EthosMarket contract using the user’s Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { marketOnchainId: number, isPositive: boolean, paymentAmount: string, minTokensOut: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/markets-v2/close-position
Close a position on a v2 market via Ethos Everywhere wallet

Submits a closePosition transaction on the EthosMarket contract using the user’s Ethos Everywhere wallet. Burns position tokens and returns WHUF. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { marketOnchainId: number, isPositive: boolean, positionTokenAmount: string, minCreditsOut: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/markets-v2/create-market
Create a v2 market via Ethos Everywhere wallet

Creates a v2 market for a subject on the EthosMarket contract using the user’s Ethos Everywhere wallet. Echo signs the creation authorization and fixes the backing amount; the EEW approves and seeds it. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { userkey: string, expectedInitialBacking: string }

Response: { hash: string }

### POST /wallets/privy/whuf/send
Send WHUF via Ethos Everywhere wallet

Submits a WHUF transfer using the user’s Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { recipient: string, amount: string }

Response: { hash: string }

### POST /wallets/privy/vouches-v2
Submit a V2 vouch via Ethos Everywhere wallet

Submits a vouch transaction on the EthosVouchV2 contract using the user’s Ethos Everywhere wallet. WHUF is pulled via an EIP-2612 permit signed server-side; no separate approval is required. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { target: string, amount: string, reviewId?: number, batchId?: string, description?: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/review-and-vouch-v2
Submit a review and vouch together via the Ethos Everywhere wallet

Posts a positive review of the target alongside a V2 vouch in a single atomic transaction via Review.reviewAndVouchWithPermit. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { target: string, comment: string, description?: string, amount: string, batchId: string }

Response: { hash: string, batchId: string }

### POST /wallets/privy/vouches-v2/increase
Increase a V2 vouch via Ethos Everywhere wallet

Adds WHUF to an existing EthosVouchV2 vouch using the user’s Ethos Everywhere wallet. WHUF is pulled via an EIP-2612 permit signed server-side covering `amount + fee`; no separate approval is required. Restricted to vouches authored by the caller’s EEW. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { vouchId: number, amount: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/vouches-v2/decrease
Decrease a V2 vouch via Ethos Everywhere wallet

Withdraws WHUF from an existing EthosVouchV2 vouch using the user’s Ethos Everywhere wallet. No permit is required — the contract refunds `amount` minus the exit fee to the author. Restricted to vouches authored by the caller’s EEW. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { vouchId: number, amount: string, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/vouches-v2/unvouch
Unvouch a V2 vouch via Ethos Everywhere wallet

Withdraws the full balance of an EthosVouchV2 vouch and archives it using the user’s Ethos Everywhere wallet. No permit is required — the contract refunds the balance minus the exit fee to the author. Restricted to vouches authored by the caller’s EEW. When `isHealthy=false`, dispatches to `unvouchUnhealthy` (combo unvouch + markUnhealthy). > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { vouchId: number, isHealthy: boolean, waitForTxTimeoutSeconds?: number }

Response: { hash: string, processed: boolean }

### POST /wallets/privy/vouches-v2/conversion/convert
Convert a V1 ETH vouch to a V2 WHUF vouch via Ethos Everywhere wallet

Unvouches the V1 ETH vouch, optionally swaps the refund to WHUF, and creates the V2 vouch for the same target under one wallet lock. Echo re-quotes with a drift guard before dispatching the run. Restricted to vouches authored by the caller’s Ethos Everywhere wallet. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Body: { v1VouchId: number, funding: "swap" | "wallet-balance", topUpAmount?: string, vouchAmount?: string, displayedMinimumBuyAmount?: string | null }

Response: { ok: true, v1VouchId: number, refundedWei: string, whufReceived: string, unvouchTxHash?: string, swapTxHashes: string[], vouchV2TxHash: string } | { ok: false, stage: "unvouch-v1" | "swap" | "vouch-v2", code: "UNVOUCH_FAILED" | "SWAP_TX_FAILED" | "VOUCH_V2_FAILED", holding: "vouch-v1" | "eth" | "whuf", refundedWei?: string, whufReceived?: string, unvouchTxHash?: string, swapTxHashes?: string[], vouchV2TxHash?: string }

### GET /wallets/lifetime-totals
Get the logged-in user’s lifetime WHUF earned and burned

Returns lifetime WHUF totals in wei across every address of the logged-in user’s profile and their embedded wallet. Earned is WHUF from reward payouts, claimed or still claimable; Burned is WHUF destroyed in vouch, review and market fees. Slashes are not counted. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Response: { burnedWei: string, earnedWei: string }

### GET /wallets/transactions
List the logged-in user’s wallet transactions

Returns the logged-in user’s on-chain transactions, newest first. Covers reputation-market buys and sells and vouch deposits and withdrawals, each with the market’s or vouch’s subject. `scope` is the Ethos Everywhere wallet alone (the default) or every address of the profile; `currency` narrows to one token. > ⚠️ **Warning:** This is a beta endpoint. It is **not guaranteed to be stable** and may change or break without notice.

Requires authentication.

Parameters:
- cursor (query, optional): number
- limit (query, optional): number
- currency (query, optional): "eth" | "whuf"
- scope (query, optional): "all" | "embedded"

Response: { nextCursor: number | null, values: ({ action: "market_buy" | "market_sell" | "unvouch" | "vouch" | "vouch_decrease" | "vouch_increase", amount: string, currency: "eth" | "whuf", id: number, side: "distrust" | "trust" | null, subject: {…} | null, timestamp: number, txHash: string })[] }

## Whuffies

WHUF token supply, balances, reward pool, price guarantee and token sale phase.

### GET /whuffies/auction-allocation
A profile’s WHUF auction allocation and referral rewards

Parameters:
- profileId (query, required): number

Response: { purchaseBase: number, xpBonusBase: number, validatorBonusBase: number, referralBonusBase: number, bonusTotalBase: number, totalBase: number, purchaseTxHash: string | null, bonusTxHash: string | null, referrals: { exclusion: …, purchaseBase: number, user: {…}, wallet: string, whufBase: number }[] } | null

### GET /whuffies/balance/{address}
Get WHUF balance for an address

Parameters:
- address (path, required): string — Ethereum address (0x-prefixed, 20 bytes)

Response: { balance: number }

### GET /whuffies/circulating-supply
Get WHUF circulating supply

Total supply less every wallet held out of circulation, as an exact decimal token amount. Add `?format=plain` for the bare number as text/plain. The excluded wallets and the formula are published at https://whitepaper.ethos.network.

Parameters:
- format (query, optional): "plain"

Response: { result: string }

### GET /whuffies/price-guarantee
Get a profile’s WHUF price guarantee

The amount of WHUF purchased in the auction that is eligible for the 85% guarantee. This amount must remain continuously vouched during the guarantee period to maintain the guarantee.

Parameters:
- profileId (query, required): number

Response: { asOf: string | null, asOfBlock: number | null, expiresAt: string | null, guaranteedWei: number | null, initialGuaranteedWei: number | null, payoutTxHashes: string[], purchasedWei: number | null, status: "active" | "expired" | "not_eligible" | "pending", unlockedAt: string | null, vouchedWei: number | null }

### GET /whuffies/reward-pool
Get the current WHUF reward rate and pool state

The contract’s current reward rate in basis points (rewardRateBps) with the pool-wide accounting behind it, read at one block and cached for 60 seconds.

Response: { accountedRewardBalance: number, blockNumber: number, emissionRateBps: number, rewardRateBps: number, timestamp: number, totalAccruedNotPaid: number, totalCommitted: number }

### GET /whuffies/token-metadata
Get WHUF token metadata

Response: { decimals: number, name: string, symbol: string }

### GET /whuffies/token-sale-state
[Deprecated] Get the current phase of the WHUF token sale

Returns a constant post-unlock state. This endpoint will be removed on or after 2026-11-12.

Response: { phase: "post-unlock", unlockedAt: string | null }

### GET /whuffies/total-supply
Get WHUF total supply

Every WHUF in existence, as an exact decimal token amount. Add `?format=plain` for the bare number as text/plain. The formula is published at https://whitepaper.ethos.network.

Parameters:
- format (query, optional): "plain"

Response: { result: string }

## Xp

Experience points: totals per user and season, timelines, and XP tips between users.

### GET /xp/user/{userkey}
Get total XP for a user across all seasons

Parameters:
- userkey (path, required): string

Response: number

### GET /xp/user/{userkey}/season/{seasonId}
Get XP for a user in a specific season

Parameters:
- userkey (path, required): string
- seasonId (path, required): number

Response: number

### GET /xp/user/{userkey}/season/{seasonId}/weekly
[Deprecated] Get weekly XP data for a user in a specific season. Use /timeline instead.

Parameters:
- userkey (path, required): string
- seasonId (path, required): number

Response: { week: number, weeklyXp: number, cumulativeXp: number }[]

### GET /xp/user/{userkey}/season/{seasonId}/timeline
Get time-bucketed XP timeline for a user with configurable granularity

Parameters:
- userkey (path, required): string
- seasonId (path, required): number
- granularity (query, required): "hour" | "day" | "week"
- since (query, optional): string

Response: { time: string, xp: number, cumulativeXp: number }[]

### GET /xp/seasons
Get all XP seasons plus current season information

Response: { seasons: { id: number, name: string, startDate: string }[], currentSeason: { id: number, name: string, startDate: string, week: number } }

### GET /xp/season/{seasonId}/weeks
Get all weeks in a season with their date ranges

Parameters:
- seasonId (path, required): number

Response: { week: number, startDate: string, endDate: string }[]

### GET /xp/tips/sent
Get XP sent by the authenticated user

Requires authentication.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ amount: number, createdAt: string, id: number, message: string | null, receiverUserId: number, senderProfileId: number, source: {…} | null })[], total: number }

### GET /xp/tips/received
Get XP received by the authenticated user

Requires authentication.

Parameters:
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ amount: number, createdAt: string, id: number, message: string | null, receiverUserId: number, senderProfileId: number, source: {…} | null })[], total: number }

### GET /xp/tips/stats
Get XP send statistics for the authenticated user

Requires authentication.

Response: { totalSent: number, totalReceived: number }

### GET /xp/history
Get XP history for a user

Parameters:
- userkey (query, required): string
- seasonId (query, optional): number
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, type: "REVIEW" | "CONTRIBUTION" | "INVITE_ACCEPTED" | "VOUCH_DAY" | "CLAIM" | "EXTENSION_CHECK_IN" | "CLAIM_REFERRAL" | "MARKET_HOLDINGS" | "ADMIN_XP_RESET" | "XP_TIP_SENT" | "XP_TIP_RECEIVED" | "VOTE_COST" | "COMMENT_COST" | "REVIEW_COST" | "STREAK_FORGIVENESS_COST" | "INVITE_SENT_COST" | "SLASH_VOTE_COST" | "VOTE_POOL_REWARD" | "COMMENT_POOL_REWARD" | "VOUCH_POOL_REWARD" | "MARKET_HOLD_POOL_REWARD" | "MARKET_TRADE_POOL_REWARD" | "DAILY_TASK_POOL_REWARD" | "XP_SLASH_ESCROW_AUTHOR" | "XP_SLASH_ESCROW_SUBJECT" | "XP_SLASH_FEE" | "XP_SLASH_STAKE_RETURNED" | "XP_SLASH_STAKE_WON" | "XP_SLASH_VOTER_REWARD" | "SLASH_VOTE_REWARD" | "SLASH_VOTE_REFUND" | "DELEGATION_REWARD" | "WEEKLY_DELEGATOR_AWARD_VALIDATOR" | "WEEKLY_DELEGATOR_AWARD_DELEGATOR" | "DECISION_SPEND_DEDUCTION" | "DECISION_NOTHING_DEDUCTION" | "DECISION_DELEGATED" | "DECISION_DELEGATION_RETURNED" | "HV_ESCROW_DEDUCTION" | "HV_BURN" | "HV_VERIFIER_PAYOUT" | "HV_COMPLETION_BONUS" | "HV_CLAWBACK" | "BOUNTY_REWARD" | "BOND_SLASH_VERIFIER_ESCROW" | "BOND_SLASH_VERIFIER_RETURNED", points: number, metadata: unknown, createdAt: string, week: number, xpSeasonId: number })[], total: number }

### GET /xp/send-details
Get XP send details by history entry IDs

Parameters:
- xpHistoryIds (query, required): number[]

Response: ({ id: number, sender: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }, receiver: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … } }, amount: number, createdAt: string, message: string | null, senderXpHistoryId: number, receiverXpHistoryId: number })[]

### GET /xp/tips
Look up XP sent by sender and/or receiver

Parameters:
- senderUserkey (query, optional): string
- receiverUserkey (query, optional): string
- limit (query, optional): number
- offset (query, optional): number

Response: { values: ({ id: number, sender: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, receiver: { id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…} }, amount: number, createdAt: string })[], total: number, limit: number, offset: number }

### GET /xp/decision
Get current profile's XP Season 2 decision

Requires authentication.

Response: { profileId: number, seasonId: number, decisionType: "SPEND" | "DELEGATE" | "NOTHING", delegations: { validatorTokenId: string, xpAmount: number }[], createdAt: string, updatedAt: string } | null

### POST /xp/decision
Submit or update XP Season 2 decision

Requires authentication.

Body: { decisionType: "SPEND" | "DELEGATE" | "NOTHING", delegations?: { validatorTokenId: string, xpAmount: number }[] }

Response: { profileId: number, seasonId: number, decisionType: "SPEND" | "DELEGATE" | "NOTHING", delegations: { validatorTokenId: string, xpAmount: number }[], createdAt: string, updatedAt: string }

### GET /xp/decision/metadata
Get XP Season 2 decision metadata (deadlines, dates)

Response: { deadline: string, delegatePercentage: number, isBeforeDeadline: boolean, maxValidators: number, minValidators: number, nothingPenalty: number, spendPercentage: number, validatorXpCap: number }

### GET /xp/validators
Get list of validator profiles for delegation

Response: ({ tokenId: string, name: string, imageUrl: string | null, ownerAddress: string, ownerProfileId: number | null, ownerDisplayName: string | null, ownerUsername: string | null, ownerAvatarUrl: string | null, currentXp: number, remainingCapacity: number, xpCap: number, isFull: boolean })[]

### GET /xp/validators/profile/{userkey}
Get validators owned by a profile with their delegators and XP info

Parameters:
- userkey (path, required): string

Response: ({ tokenId: string, name: string, imageUrl: string | null, currentXp: number, xpCap: number, delegators: { user: {…}, xpAmount: number }[] })[]

### GET /xp/dashboard/yield/{profileId}
Get yield stats for dashboard (vouch + market hold pools)

Parameters:
- profileId (path, required): number

Response: { userDailyYield: number, yesterdayDailyYield: number, yieldBreakdown: { vouchYield: number, marketHoldYield: number, marketTradeYield: number }, platformAvgYield: number, totalDailyEmissions: number, userPercentOfEmissions: number, emissionsGauge: { current: number, seasonMin: number, seasonMax: number, level: "normal" | "high" | "incredible" } }

### GET /xp/dashboard/periodic-rewards/{profileId}
Get periodic reward stats for dashboard (daily tasks, votes, comments)

Parameters:
- profileId (path, required): number

Response: { today: { totalPool: number, userEarnings: number, topEarners: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…}, points: number })[] }, yesterday: { totalPool: number, userEarnings: number, topEarners: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: …[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: …, validatorNftCount: number, meowMeowsNftCount: number, links: {…}, stats: {…}, points: number })[] }, userPercentOfRewards: number, validatorStaking: { totalDelegated: number, validators: ({ tokenId: string, name: string, imageUrl: string | null, xpAmount: number })[] }, nextDistributionDate: string, weeklyTotals: { votePoolReward: number, commentPoolReward: number, vouchPoolReward: number, marketHoldPoolReward: number, marketTradePoolReward: number, delegationReward: number, slashStakeReturned: number, slashStakeWon: number, slashVoterReward: number, slashVoteReward: number, slashVoteRefund: number, total: number }, validatorRewards: { today: number, yesterday: number, weekly: number } }

### GET /xp/dashboard/multipliers/{profileId}
Get multiplier stats for dashboard (score, streak, validators, market holdings)

Parameters:
- profileId (path, required): number

Response: { scoreMultiplier: { value: number, score: number, tier: string, nextTier: { threshold: number, multiplier: number } | null }, streakMultiplier: { value: number, streakDays: number, tier: string, nextTier: { threshold: number, multiplier: number } | null }, validatorCount: number, marketHoldingsEth: number, combinedMultiplier: number }

### GET /xp/dashboard/daily-earnings/{profileId}/{date}
Get user daily earnings breakdown

Parameters:
- profileId (path, required): number
- date (path, required): "today" | "yesterday"

Response: { items: ({ id: number, type: "REVIEW" | "CONTRIBUTION" | "INVITE_ACCEPTED" | "VOUCH_DAY" | "CLAIM" | "EXTENSION_CHECK_IN" | "CLAIM_REFERRAL" | "MARKET_HOLDINGS" | "ADMIN_XP_RESET" | "XP_TIP_SENT" | "XP_TIP_RECEIVED" | "VOTE_COST" | "COMMENT_COST" | "REVIEW_COST" | "STREAK_FORGIVENESS_COST" | "INVITE_SENT_COST" | "SLASH_VOTE_COST" | "VOTE_POOL_REWARD" | "COMMENT_POOL_REWARD" | "VOUCH_POOL_REWARD" | "MARKET_HOLD_POOL_REWARD" | "MARKET_TRADE_POOL_REWARD" | "DAILY_TASK_POOL_REWARD" | "XP_SLASH_ESCROW_AUTHOR" | "XP_SLASH_ESCROW_SUBJECT" | "XP_SLASH_FEE" | "XP_SLASH_STAKE_RETURNED" | "XP_SLASH_STAKE_WON" | "XP_SLASH_VOTER_REWARD" | "SLASH_VOTE_REWARD" | "SLASH_VOTE_REFUND" | "DELEGATION_REWARD" | "WEEKLY_DELEGATOR_AWARD_VALIDATOR" | "WEEKLY_DELEGATOR_AWARD_DELEGATOR" | "DECISION_SPEND_DEDUCTION" | "DECISION_NOTHING_DEDUCTION" | "DECISION_DELEGATED" | "DECISION_DELEGATION_RETURNED" | "HV_ESCROW_DEDUCTION" | "HV_BURN" | "HV_VERIFIER_PAYOUT" | "HV_COMPLETION_BONUS" | "HV_CLAWBACK" | "BOUNTY_REWARD" | "BOND_SLASH_VERIFIER_ESCROW" | "BOND_SLASH_VERIFIER_RETURNED", points: number, metadata: {…} | null, createdAt: string })[] }

### GET /xp/dashboard/daily-awards/{date}
Get daily awards (top activities and resolved slashes)

Parameters:
- date (path, required): "today" | "yesterday"

Response: { topEarners: ({ id: number, profileId: number | null, displayName: string, username: string | null, avatarUrl: string, description: string | null, score: number, status: "ACTIVE" | "INACTIVE" | "MERGED", userkeys: string[], xpTotal: number, xpStreakDays: number, xpRemovedDueToAbuse: boolean, influenceFactor: number, influenceFactorPercentile: number, humanVerificationStatus?: "REQUESTED" | "VERIFIED" | "REVOKED" | "PENDING" | null, validatorNftCount: number, meowMeowsNftCount: number, links: { profile: string, scoreBreakdown: string }, stats: { holdingsWei: …, review: {…}, vouch: {…}, vouchTotals: …[], whufGuarantee: … }, points: number })[], topActivities: { activity: object, dayOfVotes: number }[], resolvedSlashes: ({ slashId: number, outcome: string, slashVotes: number, defendVotes: number, subject: {…} | null })[] }

### GET /xp/dashboard/weekly-delegator-award/{profileId}
Get weekly delegator award stats for dashboard

Parameters:
- profileId (path, required): number
- week (query, optional): number

Response: { lastAward: { delegatorCount: number, delegatorsXpAward: number, distributedAt: string, topDelegators: {…}[], ownerXpAward: number, week: number, winner: { owner: …, validator: {…} } } | null, lifetime: { totalEarned: number, timesSelected: number }, nextAwardDate: string, userParticipation: { delegatedToWinner: boolean, xpEarned: number } }
