openapi: 3.1.0
info:
  title: AtlasYield API
  version: "1.0"
  description: >-
    Read-only access to the Atlas Score Index: neutral 16-factor vault scores across four
    pillars — Yield, Safety, Liquidity, Sustainability — plus the tracked vault catalog
    and the Atlas Engine, which turns those scores into a risk-adjusted allocation.
    Research and information, not investment advice.
servers:
  - url: https://api.atlasyield.club/v1
components:
  schemas:
    Score:
      type: object
      properties:
        vaultId:
          type: string
          description: "Stable identifier: protocol:chainId:address."
          example: "aave-v3:8453:0x4e65fe4dba92790696d040ac24aa414708f5c0ab"
        chainId:
          type: integer
          description: EVM chain id (e.g. 1 Ethereum, 8453 Base, 42161 Arbitrum).
          example: 8453
        protocolId:
          type: string
          description: >-
            Adapter/protocol slug. Currently one of beefy, yearn-v3, morpho, pendle,
            aave-v3, euler, etherfi, ethena. New adapters are added over time — treat
            this as an open set rather than a fixed enum.
          example: aave-v3
        name:
          type: string
          nullable: true
          description: >-
            Human-readable vault name, the same string /catalog reports, so no join is
            needed. Null only when the vault is absent from the live catalog cache.
          example: "Aave V3 USDC"
        asset:
          type: string
          description: Underlying asset symbol.
          example: USDC
        assetClass:
          type: string
          nullable: true
          enum: [stablecoin, stablecoin-yield, eth, btc, top50, usd-exotic, other]
          description: Asset class bucket used to contextualise the yield factors. May be null.
          example: stablecoin
        composite:
          type: number
          description: 0–100 composite Atlas Score.
          example: 87.5
        label:
          type: string
          enum: [Conservative, Balanced, Aggressive]
          description: Risk-posture band for the composite.
        pillarScores:
          type: object
          description: "The four pillar scores, 0–100 each."
          properties:
            yield: { type: number, example: 82.0 }
            safety: { type: number, example: 90.0 }
            liquidity: { type: number, example: 88.0 }
            sustainability: { type: number, example: 85.0 }
        apy:
          type: number
          nullable: true
          description: Current APY in percentage points (5.21 = 5.21%). May be null.
          example: 5.21
        tvl:
          type: number
          nullable: true
          description: Total value locked, USD. May be null.
          example: 1250400
        dataQuality:
          type: string
          enum: [sufficient, partial, fallback]
          description: How complete the inputs were.
        scoredAt:
          type: string
          format: date-time
          description: ISO-8601 UTC timestamp the score was computed (ex-ante, immutable).
        scorerVersion:
          type: string
          description: Scoring engine version that produced the row.
          example: "2.1.0"
    CatalogEntry:
      type: object
      properties:
        id:
          type: string
          description: Vault identifier (same as vaultId in /scores).
          example: "aave-v3:8453:0x4e65fe4dba92790696d040ac24aa414708f5c0ab"
        chainId:
          type: integer
          example: 8453
        protocolId:
          type: string
          example: aave-v3
        protocol:
          type: string
          description: Human-readable protocol name.
          example: Aave V3
        asset:
          type: string
          example: USDC
        name:
          type: string
          description: Display name (protocol + asset).
          example: Aave V3 USDC
        tvl:
          type: number
          description: Total value locked, USD.
          example: 1250400
        apy:
          type: number
          description: Current APY in percentage points.
          example: 5.21
    PortfolioSlice:
      type: object
      description: One position in the proposed allocation.
      properties:
        vaultId:
          type: string
          description: Vault identifier (same as vaultId in /scores).
          example: "morpho:1:0xbeefc1cdafc5b4a649b54d07afc6bf0f75c6f4e2"
        protocol:
          type: string
          example: morpho
        chainId:
          type: integer
          example: 1
        asset:
          type: string
          description: Underlying asset symbol.
          example: USDC
        name:
          type: string
          description: Vault display name.
          example: STEAKUSDTBETHENA
        apy:
          type: number
          description: Current APY in percentage points.
          example: 12.18706
        tvl:
          type: number
          description: Total value locked, USD.
          example: 11780173
        composite:
          type: number
          description: The vault's 0–100 Atlas Score at allocation time.
          example: 78.762
        fraction:
          type: number
          description: Share of principal as a fraction, rounded to 1e-4.
          example: 0.15
        allocationPct:
          type: number
          description: Share of principal as a percentage, 2 dp.
          example: 15
        allocationUsd:
          type: number
          description: Dollar amount allocated to this vault.
          example: 1500
        vaultAddress:
          type: string
          description: On-chain vault contract address.
          example: "0xbeefc1cdafc5b4a649b54d07afc6bf0f75c6f4e2"
        underlyingToken:
          type: string
          description: Address of the token the vault accepts.
          example: "0xc139190f447e929f090edeb554d95abb8b18ac1c"
    FactorResult:
      type: object
      properties:
        score: { type: number, description: "Sub-score 0–100. For EXPLOIT_HISTORY this is multiplier × 100.", example: 82 }
        weight: { type: number, description: "Weight inside its pillar's arithmetic mean. 0 for EXPLOIT_HISTORY (multiplicative).", example: 0.3 }
        rawValue: { type: number, description: "The raw input that drove the score (TVL USD, audit count, slope, ...).", example: 5.2 }
        label: { type: string, description: "Plain-English description of the input.", example: "5.2% APY, stablecoin" }
        pillar: { type: string, enum: [yield, safety, liquidity, sustainability] }
    VaultFactors:
      type: object
      properties:
        vaultId: { type: string, example: "aave-v3:8453:0x4e65fe4dba92790696d040ac24aa414708f5c0ab" }
        chainId: { type: integer, example: 8453 }
        protocolId: { type: string, example: aave-v3 }
        composite: { type: number, example: 87.5 }
        label: { type: string, enum: [Conservative, Balanced, Aggressive] }
        pillarScores:
          type: object
          properties:
            yield: { type: number }
            safety: { type: number }
            liquidity: { type: number }
            sustainability: { type: number }
        factors:
          type: object
          description: >-
            All 16 factors keyed by name: YIELD_LEVEL, APY_STABILITY, APY_PERSISTENCE,
            YIELD_SOURCE, AUDIT_QUALITY, PROTOCOL_MATURITY, TVL_DEPTH_SAFETY, TVL_STABILITY,
            ADMIN_TOPOLOGY, CHAIN_SECURITY, EXPLOIT_HISTORY, WITHDRAWAL_TYPE,
            TVL_DEPTH_LIQUIDITY, EMISSION_DEPENDENCY, ASSET_RISK, UTILIZATION_HEALTH.
          additionalProperties:
            $ref: "#/components/schemas/FactorResult"
        dataQuality: { type: string, enum: [sufficient, partial, fallback] }
        scoredAt: { type: string, format: date-time }
        scorerVersion: { type: string, example: "2.1.0" }
    VaultMetrics:
      type: object
      description: >-
        Tier 1 live metrics for one vault. Separate from the Atlas Score, never folded
        into the composite.
      properties:
        vaultId:
          type: string
          description: "Stable identifier: protocol:chainId:address."
          example: "aave-v3:8453:0x4e65fe4dba92790696d040ac24aa414708f5c0ab"
        chainId:
          type: integer
          example: 8453
        yieldQuality:
          type: number
          nullable: true
          description: >-
            0-100. How much of the headline APY comes from the vault's own economics
            rather than transient incentives. Null when the APY split is unavailable.
          example: 82
        yieldQualityReason:
          type: string
          nullable: true
          description: Why that value was assigned, or why it is null.
        exitSafety:
          type: number
          nullable: true
          description: >-
            0-100. How safely a position can be exited at size. Null when the underlying
            liquidity data is unavailable.
          example: 74
        exitSafetyReason:
          type: string
          nullable: true
          description: Why that value was assigned, or why it is null.
        scorerRunDate:
          type: string
          format: date
          description: The day (UTC) these metrics were computed.
          example: "2026-09-04"
    VaultRoute:
      type: object
      description: >-
        Round-trip route-survival verdict for one vault: send USDC in, take the
        position, sell it straight back, on the exact path the app itself executes.
        "Can I get out at size, measured today" — not a liquidity claim, a real quote.
        SAME-CHAIN ONLY: funding USDC sits on the vault's own chain. Nothing here
        describes bridging in from another chain (see `probe`).
      properties:
        vaultId:
          type: string
          nullable: true
          description: "Stable identifier: protocol:chainId:address. Null when unscreened."
          example: "pendle:1:0x5949c3bfdf9babd8bf011d9d8dbb1a2c3e10d8b8"
        chainId:
          type: integer
          example: 1
        screened:
          type: boolean
          description: False when this vault has never been screened — no verdict is fabricated.
          example: true
        verdict:
          type: string
          nullable: true
          enum: [OK, DEGRADED, DANGEROUS, UNPRICEABLE, NO_ROUTE, SKIPPED, null]
          description: >-
            OK/DEGRADED are tradable. DANGEROUS, UNPRICEABLE and NO_ROUTE block deposits
            in the live engine. SKIPPED means our own probe was rate-limited, not a
            statement about the vault. Null when unscreened.
          example: OK
        reasonClass:
          type: string
          nullable: true
          enum: [protocol_refused, no_aggregator_quote, dust, no_amount, rate_limited, unmapped, null]
          description: >-
            Why a non-OK verdict was reached. `protocol_refused`: the protocol's own
            router (Pendle Convert) refuses the position — the vault is dead for entry
            or exit. `no_aggregator_quote`: no same-chain aggregator quotes USDC into
            the underlying (typical for LP-token vaults); the vault may still be
            exitable by redeeming and unwinding by hand. `dust`: value came back but
            below the 85% floor. Null for OK/DEGRADED.
          example: protocol_refused
        usdIn:
          type: number
          nullable: true
          description: USDC sent on the worst probed notional.
          example: 5000
        usdOut:
          type: number
          nullable: true
          description: USDC that came back after entering and exiting the position.
          example: 4991
        retained:
          type: number
          nullable: true
          description: usdOut / usdIn on the worst probed notional.
          example: 0.9982
        path:
          type: string
          nullable: true
          enum: [pendle, composer, native, null]
          description: Which execution path was probed — the same one the app itself calls.
          example: pendle
        probe:
          type: object
          description: >-
            What was measured, stated on every response. `scope` is always
            `same-chain`; `crossChain` is always `not probed`.
          properties:
            scope: { type: string, example: same-chain }
            funding: { type: string }
            legs: { type: string }
            notionalsUsd: { type: array, items: { type: number }, example: [100, 5000] }
            router: { type: object, additionalProperties: { type: string } }
            crossChain: { type: string, example: not probed }
        checkedAt:
          type: string
          format: date-time
          nullable: true
          description: When this verdict was last measured. Null when unscreened.
  responses:
    ValidationError:
      description: >-
        A query parameter is missing or out of range. The body carries per-field
        detail under `details.fieldErrors`.
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean, example: false }
              error: { type: string, example: "Validation error" }
              details:
                type: object
                description: Zod field errors, keyed by parameter name.
paths:
  /scores:
    get:
      operationId: getScores
      summary: Latest scores
      description: >-
        The most recent composite + pillar scores for every tracked vault — one row per
        vault, newest first.
      parameters:
        - name: chainId
          in: query
          required: false
          schema: { type: integer }
          description: Filter to one EVM chain id.
          example: 8453
        - name: protocolId
          in: query
          required: false
          schema: { type: string }
          description: Filter to one protocol slug.
          example: morpho
      responses:
        "200":
          description: Latest scores for all tracked vaults.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  scorerVersion: { type: string, example: "2.1.0" }
                  count: { type: integer, example: 570 }
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Score"
  /catalog:
    get:
      operationId: getCatalog
      summary: Vault catalog
      description: >-
        The tracked vault universe — every vault Atlas scores, with its live APY and TVL.
        Use it to discover vaultIds before querying /scores.
      parameters:
        - name: chainId
          in: query
          required: false
          schema: { type: integer }
          description: Filter to one EVM chain id.
          example: 8453
        - name: protocolId
          in: query
          required: false
          schema: { type: string }
          description: Filter to one protocol slug.
          example: morpho
      responses:
        "200":
          description: The tracked vault universe.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  count: { type: integer, example: 1076 }
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CatalogEntry"
  /portfolio:
    get:
      operationId: getPortfolio
      summary: Build a portfolio
      description: >-
        The Atlas Engine as a service. Give it a risk tier, a timeline, and a principal;
        it runs the mean-variance allocator over the current scored universe and returns
        the resulting allocation, sized in dollars.


        Unlike /scores and /catalog, this endpoint **fails closed on stale scoring**: an
        allocation is an active recommendation, so if the most recent scoring run is too
        old the endpoint returns 503 rather than allocating on stale data.


        The allocator is deterministic — the same inputs against the same scoring
        snapshot return the same portfolio.
      parameters:
        - name: riskTier
          in: query
          required: true
          schema:
            type: string
            enum: [conservative, balanced, aggressive]
          description: Which tier policy to allocate under. See the tier policy page.
          example: balanced
        - name: principal
          in: query
          required: true
          schema:
            type: number
            minimum: 100
            maximum: 1000000
          description: Amount to allocate, USD. Must be between 100 and 1,000,000.
          example: 10000
        - name: timeline
          in: query
          required: false
          schema:
            type: string
            enum: [short, medium, long]
            default: medium
          description: >-
            Investment horizon. Nudges risk appetite within the tier's band — short is
            more variance-averse, long more return-seeking — and a short horizon also
            tightens the cap on vaults with little history.
          example: medium
      responses:
        "200":
          description: The proposed allocation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  riskTier: { type: string, example: balanced }
                  timeline: { type: string, example: medium }
                  principal: { type: number, example: 10000 }
                  strategy:
                    type: string
                    enum: [MVO, naive, none]
                    description: >-
                      Which allocator produced the result. MVO is the mean-variance
                      optimiser; naive is the equal-weight fallback; none means no
                      allocation could be produced (see `note`).
                    example: MVO
                  fallback:
                    type: boolean
                    description: True when the result came from the fallback path rather than MVO.
                    example: false
                  allocations:
                    type: array
                    items:
                      $ref: "#/components/schemas/PortfolioSlice"
                  expectedApy:
                    type: number
                    nullable: true
                    description: Principal-weighted expected APY of the allocation, percentage points.
                    example: 4.69
                  expectedSharpe:
                    type: number
                    description: Expected Sharpe-like ratio of the allocation.
                    example: 14.58
                  unallocatedUsd:
                    type: number
                    description: >-
                      Principal left in cash because policy caps could not be satisfied.
                      Surfaced honestly rather than force-fitted into a vault.
                    example: 0
                  scorerVersion: { type: string, example: "2.1.0" }
                  generatedAt:
                    type: string
                    format: date-time
                    description: ISO-8601 UTC timestamp the allocation was produced.
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  note:
                    type: string
                    description: Present only when the allocator could not fully allocate; explains why.
        "400":
          $ref: "#/components/responses/ValidationError"
        "503":
          description: >-
            Scoring data is stale, so no allocation was produced. Carries a `Retry-After`
            header (seconds).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: false }
                  error:
                    type: string
                    example: "Scoring data is stale — portfolio unavailable"
  /basket:
    post:
      operationId: scoreBasket
      summary: Score a basket
      description: >-
        Score a basket you define. Send up to 50 positions (chainId + vault address,
        optional weight) and get back each position's current Atlas Score plus a
        weighted basket score: composite, pillars, and APY, weighted by position size.
        Weights are normalised, so percentages, USD amounts, and fractions all work;
        omit them for equal weight.


        **Coverage is disclosed, never assumed.** A position Atlas does not score is
        returned with `scored: false` and excluded from the aggregate, and
        `basket.scoredWeight` reports the share of the basket the score actually
        covers. It is never averaged in as zero.


        A pure computation over the request body: nothing is stored.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positions]
              properties:
                positions:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [chainId, address]
                    properties:
                      chainId: { type: integer, example: 1 }
                      address:
                        type: string
                        description: Vault contract address, case-insensitive.
                        example: "0xd8a9f8b3c1b52e8a4f1e2f6c4e0b1a9c8d7e6f5a"
                      weight:
                        type: number
                        exclusiveMinimum: 0
                        description: Position size in any unit; normalised across the basket. Omit on every position for equal weight.
                        example: 60
      responses:
        "200":
          description: The basket score and each position.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  disclaimer: { type: string, example: "Research and information, not investment advice." }
                  scorerVersion: { type: string, example: "2.1.0" }
                  generatedAt: { type: string, format: date-time }
                  basket:
                    type: object
                    properties:
                      composite:
                        type: number
                        nullable: true
                        description: Weight-weighted mean of the scored positions' composites. Null when nothing in the basket is scored.
                        example: 74.2
                      label: { type: string, nullable: true, example: Good }
                      pillarScores:
                        type: object
                        properties:
                          yield: { type: number, nullable: true }
                          safety: { type: number, nullable: true }
                          liquidity: { type: number, nullable: true }
                          sustainability: { type: number, nullable: true }
                      apy:
                        type: number
                        nullable: true
                        description: Weight-weighted live APY of the scored positions, percentage points.
                      positions: { type: integer, description: Positions sent. }
                      scoredPositions: { type: integer, description: Positions Atlas scores. }
                      scoredWeight:
                        type: number
                        description: Share of basket weight covered by the aggregate, 0 to 1. Below 1 means the basket holds something Atlas does not score.
                        example: 1
                      maxWeight: { type: number, description: Largest single position weight, 0 to 1. }
                      chainShare:
                        type: object
                        additionalProperties: { type: number }
                        description: Basket weight by chainId.
                      protocolShare:
                        type: object
                        additionalProperties: { type: number }
                        description: Basket weight by protocolId; unscored positions appear under `unscored`.
                  positions:
                    type: array
                    description: One entry per position sent, in order. Scored entries carry the same fields as a /scores row plus `weight`.
                    items:
                      type: object
                      properties:
                        chainId: { type: integer }
                        address: { type: string }
                        weight: { type: number, description: Normalised weight, 0 to 1. }
                        scored: { type: boolean }
                        vaultId: { type: string }
                        name: { type: string, nullable: true }
                        protocolId: { type: string }
                        composite: { type: number }
                        label: { type: string }
                        apy: { type: number, nullable: true }
                        tvl: { type: number, nullable: true }
                        dataQuality: { type: string, enum: [sufficient, partial, fallback] }
                        scoredAt: { type: string, format: date-time }
        "400":
          $ref: "#/components/responses/ValidationError"
  /vaults/{chainId}/{address}/factors:
    get:
      operationId: getVaultFactors
      summary: 16-factor breakdown for one vault
      description: >-
        The "why" behind a composite: every one of the 16 factors (15 additive across four
        pillars plus the multiplicative exploit-history modifier) with its sub-score, weight,
        raw input and a plain-English label. Served from the stored score row, so it is exactly
        what the engine computed at `scoredAt`, not a recomputation.
      parameters:
        - name: chainId
          in: path
          required: true
          schema: { type: integer }
          example: 8453
        - name: address
          in: path
          required: true
          schema: { type: string }
          description: Vault contract address. Case-insensitive.
          example: "0x4e65fe4dba92790696d040ac24aa414708f5c0ab"
      responses:
        "200":
          description: The latest score row's factor breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  data:
                    $ref: "#/components/schemas/VaultFactors"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          description: The vault is not in the scored universe.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: false }
                  error: { type: string, example: "No score found for this vault" }
  /vaults/{chainId}/{address}/metrics:
    get:
      operationId: getVaultMetrics
      summary: Live risk metrics for one vault
      description: >-
        Tier 1 live metrics for a single vault: yield quality and exit safety, each with
        the reason it was assigned. These are **separate from the Atlas Score** and are
        never folded into it — the composite answers "how good is this vault", these two
        answer "is the yield real" and "can I get out".


        Both values are nullable and are never fabricated: when the underlying inputs are
        missing, the value is `null` and the reason says why. Metrics are recomputed on a
        ~24h cadence, so `scorerRunDate` is the day the values were produced.
      parameters:
        - name: chainId
          in: path
          required: true
          schema: { type: integer }
          description: EVM chain id the vault lives on.
          example: 8453
        - name: address
          in: path
          required: true
          schema: { type: string }
          description: Vault contract address. Case-insensitive.
          example: "0x4e65fe4dba92790696d040ac24aa414708f5c0ab"
      responses:
        "200":
          description: The most recent metrics row for the vault.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  data:
                    $ref: "#/components/schemas/VaultMetrics"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          description: No metrics have been computed for this vault.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: false }
                  error:
                    type: string
                    example: "Risk metrics not found for this vault"
  /vaults/{chainId}/{address}/route:
    get:
      operationId: getVaultRoute
      summary: Route-survival verdict for one vault
      description: >-
        The wedge, not the score. An agent about to move real money needs "can I get
        out at size, measured today" more than "is this good". This is the same verdict
        `refreshAllRouteScreens` writes daily and the live engine uses to gate deposits —
        exposed read-only. Round trip, on the protocol's own execution path, so no
        pricing oracle is involved: USDC compared against USDC.


        A vault that has never been screened returns `screened: false` with every
        measurement field `null` — never a fabricated verdict.
      parameters:
        - name: chainId
          in: path
          required: true
          schema: { type: integer }
          description: EVM chain id the vault lives on.
          example: 1
        - name: address
          in: path
          required: true
          schema: { type: string }
          description: Vault contract address. Case-insensitive.
          example: "0x5949c3bfdf9babd8bf011d9d8dbb1a2c3e10d8b8"
      responses:
        "200":
          description: The most recent route-survival verdict for the vault, or the unscreened shape.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  disclaimer:
                    type: string
                    example: "Research and information, not investment advice."
                  data:
                    $ref: "#/components/schemas/VaultRoute"
        "400":
          $ref: "#/components/responses/ValidationError"
