Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: ASPERN Crowding API
version: '0.2'
summary: The independent record of what machine-managed capital actually does.
x-performance:
simpleReads:
p95Ms: 500
meaning: A list or a lookup, warm.
heavyAnalytics:
p95Ms: 2000
meaning: Correlation, the index, stress and cascade, warm.
coldPath:
meaning: 'The first call after a cache expires. Not covered by the figures above. The deploy and a five-minute timer warm the heavy free endpoints, so a caller should rarely meet one there. `/v1/correlation` is the exception and is the slowest: it needs a capability, so the warmer cannot reach past the gate without holding a key, and putting a credential in the web process to solve a latency problem is a trade we have not made. Expect several seconds on the first call after an expiry.'
timeoutSeconds: 60
maxWindowDays: 400
description: 'Ratings are free. The data, the analytics and the certification are not.
Every response states what it cannot see. Coverage, sample size and the mechanism behind a figure travel with the figure rather than sitting in documentation, because a number read without them is the failure this platform exists to prevent.'
x-aspern-environment: production
x-aspern-build: 20261101T2237Z-27da4c8c29
x-contract-hash: 6c89cbaf2e2c50bd2b65bddf516e17b855704d01e471a6c06c19354fedc7367d
x-stability:
policy: additive-within-major
promise: 'Within /v1 the contract only grows: new routes, optional parameters, fields and warning codes. Routes and fields are never removed or renamed, and no type, unit or status meaning changes.'
deprecation:
marker: 'x-lifecycle: deprecated, with the replacement named'
minimumDays: 180
removal: only in a new major version; /v1 keeps answering through the deprecation period
verification: checked daily against the approved operation inventory; every change of x-contract-hash is recorded
changelog: https://aspern.org/data/reference#changelog
servers:
- url: https://aspern.org/api
security:
- apiKey: []
tags:
- name: Crowding
paths:
/v1/collateral/{symbol}:
get:
tags:
- Crowding
summary: Which vaults lend against one collateral, and at what size
description: 'PAID. The vault answer carries the TOTALS free - how many vaults share the exposure and how much they hold - and this is which ones they are, the same split the per-market rows already make. The denominator is THE VAULTS WE TRACK and never a market: nobody publishes how much of an asset is pledged across all of lending, so this says who falls together rather than how large the fall would be. A vault lending against this asset on a venue we do not walk is absent from the crowd rather than counted at zero, and a slice named `idle` is the absence of an exposure and joins no crowd.
Requires `crowding.members` — PRO or ENTERPRISE. Without it the call answers 402 with the amount and a deposit address.'
operationId: getCollateralBySymbol
responses:
'200':
description: The answer, with its own limitations stated inline.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
content:
application/json:
schema:
$ref: '#/components/schemas/CollateralCrowdDocument'
'400':
description: A parameter is present and unreadable. `invalid_params` names every one at fault, not just the first.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
'401':
description: 'A key was supplied and is unknown, revoked or expired. Sending no key at all is not this — that is the free tier. This response carries the full rate-limit headers for the ANONYMOUS bucket, which is the bucket the caller is in: an unrecognised key is throttled by address before it is refused, and no customer plan is named because there is no customer.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
'402':
description: This capability is not carried by the caller’s tier. The body is the x402 payment offer — capability, amount and where to send it — not an error document.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
content:
application/json:
schema:
$ref: '#/components/schemas/PaymentRequired'
'404':
description: Nothing of that name, or too little data to answer. The detail says which.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
'429':
description: Rate or allowance exhausted. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` are on every response; `Retry-After` is on this one.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
RateLimit-Reset:
$ref: '#/components/headers/RateLimitReset'
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
Retry-After:
$ref: '#/components/headers/RetryAfter'
'500':
description: Something failed at our end. Quote `request_id` — it is on every response and in our logs.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
parameters:
- name: symbol
in: path
required: true
description: The asset symbol as the venue writes it, e.g. wstETH. Case is ignored.
schema:
type: string
x-lifecycle: active
components:
headers:
RateLimitPolicy:
description: 'Both policies, comma-separated, in the order a client needs them: `<burst>;w=<seconds to refill>;name="burst", <perMinute>;w=60;name="<plan>"`. The first is what may be sent right now and is what the counters above describe; the second is what may be sustained and is the figure on the price list.'
schema:
type: string
required: true
RateLimitLimit:
description: How many requests the burst bucket holds when full. This is the limit that refuses a request, so it is the one these counters describe. The sustained per-minute allowance is the second entry in `RateLimit-Policy`, and the monthly quota is on `GET /v1/account`.
schema:
type: integer
required: true
RequestId:
description: The id of this request, also on every problem document as `request_id`. Quote it and we can find the call.
schema:
type: string
required: true
RetryAfter:
description: Seconds to wait before retrying. Sent on a refusal.
schema:
type: integer
RateLimitRemaining:
description: Requests left in the bucket right now. Send more than this and the next one is refused.
schema:
type: integer
required: true
RateLimitReset:
description: SECONDS FROM NOW until the bucket is FULL again, not a timestamp, and not the wait for a single request — that is `Retry-After`, sent only on a refusal. Zero means it is already full and there is nothing to wait for. A client treating it as epoch seconds sleeps until 1970 and hammers us instead.
schema:
type: integer
required: true
schemas:
Meta:
type: object
required:
- requestId
- snapshotId
- servedAt
- methodologyVersion
- coverage
properties:
requestId:
type: string
description: Also the X-Request-Id header.
environment:
type: string
description: production, staging or development. Also the X-ASPERN-Environment header and info.x-aspern-environment in the OpenAPI document, so a client can tell which contract answered.
build:
type:
- string
- 'null'
description: The deploy that served this. Also the X-ASPERN-Build header.
snapshotId:
type: string
description: Which state of our data answered. Two responses with the same snapshotId saw the same completed ingest runs — so a figure that differs between two surfaces under the SAME snapshot is a defect, and under different ones is a clock.
observedAt:
type:
- string
- 'null'
format: date-time
description: When the source was observed.
computedAt:
type:
- string
- 'null'
format: date-time
description: When we computed this from it.
servedAt:
type: string
format: date-time
description: When this response was built.
methodologyVersion:
type: string
description: What the figures mean. Changes are listed at /changelog.
source:
type:
- string
- 'null'
chainId:
type:
- integer
- 'null'
x-unit: chain-id
blockNumber:
type:
- integer
- 'null'
x-unit: block-number
coverage:
type: object
properties:
scope:
type:
- string
- 'null'
description: What this covers and, explicitly, what it does not.
population:
type:
- string
- 'null'
description: The population these figures were measured over, as a key you can join on rather than a sentence you have to read. Every key is declared once and resolves to a definition at /reconciliation#<key>, where the ones that narrow show every row that falls out and why. The same key appears beside the figure on the website, so an integrator and a reader are talking about one population rather than two descriptions of it. Null only when `populationAbsence` says why.
populationAbsence:
type:
- string
- 'null'
description: Why there is no single population, where there is none. A lookup of one address measures nothing; a document carrying several rails in units that are never summed carries several populations. A null population with nothing here would be an absence you could not tell from an oversight, which is the defect this field exists to close.
knownExclusions:
type:
- array
- 'null'
items:
type: string
description: THREE VALUES. A list is what this route deliberately leaves out. `[]` is a CLAIM that nothing is excluded. `null` is “we have not said”, which is our gap and must never be read as the second. It was hardcoded to `[]` on every route until 2026-09-27, so every response claimed to exclude nothing while its own `scope` sentence named exclusions.
confidence:
type:
- object
- 'null'
description: Never a bare number; it names what it is a confidence in.
freshness:
anyOf:
- type: object
required:
- oldestObservedAt
- newestObservedAt
- readings
- fields
- complete
- state
- newestAgeHours
description: When the readings behind this answer were taken, where they differ. A page of a hundred vaults read over four hours has no single observation time — `observedAt` is filled in only where every reading agrees, and this reports the spread otherwise.
properties:
oldestObservedAt:
type: string
format: date-time
newestObservedAt:
type: string
format: date-time
readings:
type: integer
minimum: 1
description: How many readings the range was taken over.
x-unit: count
fields:
type: array
items:
type: string
description: The fields it was read from, so a caller can go and check any of them directly.
complete:
type: boolean
description: Whether every reading in the payload was seen. False means the range is a LOWER BOUND — the true spread is at least this wide and may be wider — because a partial walk always returns the narrower range, which would read as fresher and more uniform than the data is.
state:
enum:
- fresh
- mixed
- stale
description: '`stale` when the NEWEST reading is more than 26 hours old at the moment the response was built — every reader runs at least daily, so that means a reader has stopped. `mixed` when the newest is inside that window and the OLDEST is not: parts of this answer are current and parts are not, which is the usual shape of a document joining several sources. It was summarised as `fresh` until 2026-10-10, computed from the newest reading alone, so one current field hid a catalogue two months old. A source that could not be read is not a state here: it is a check that could not run (`/v1/checks`) or an analysis whose `availability` says so, and never a zero.'
newestAgeHours:
type: number
minimum: 0
description: Hours between the newest reading and the moment this response was built, to one decimal.
x-unit: hours
oldestAgeHours:
type: number
minimum: 0
description: The same for the OLDEST reading. The other end of the spread as a number, because a caller comparing two ISO strings to find out whether part of an answer is two months old will mostly not bother.
x-unit: hours
- type: object
required:
- reason
- state
- says
- readings
description: No range, and which of the two reasons it is.
properties:
reason:
enum:
- not-applicable
- no-dated-field
description: '`not-applicable` is reference material: a plan, a capability list, a schema. Nothing in it was read from anywhere at a time. `no-dated-field` means the response carries data and nothing in it names when that data was read, which is a gap in that endpoint.'
state:
enum:
- not_applicable
- not_measured
description: 'The reason in the vocabulary shared with `fresh`/`stale`: `not-applicable` is `not_applicable`, `no-dated-field` is `not_measured`.'
says:
type: string
description: The same thing in a sentence.
readings:
const: 0
pagination:
anyOf:
- $ref: '#/components/schemas/Page'
- type: 'null'
CollateralCrowdDocument:
type: object
description: Who lends against one collateral. The denominator is the vaults we track and never a market.
required:
- data
- meta
- warnings
properties:
data:
type: object
properties:
collateral:
type: string
description: The asset borrowers pledge, as the venue writes it.
readAt:
type:
- string
- 'null'
description: When the crowds were last built.
vaults:
type: integer
minimum: 0
description: Open vaults WE TRACK that lend against it.
x-unit: count
suppliedUsd:
type: number
description: What they have supplied between them, in US dollars.
x-unit: usd
venues:
type: array
items:
type: string
description: The venues the crowd spans, biggest first. A crowd on one venue is one venue’s problem.
members:
type: array
items:
type: object
description: Each vault in the crowd with what it supplied and its share OF THE CROWD. This is the paid half; the totals above are free on the vault answer.
membersListed:
type: integer
minimum: 0
description: Beside `vaults`, so a reader who sees fewer rows than vaults is owed the reason rather than left to assume a truncation.
limits:
type: string
required:
- collateral
- vaults
- suppliedUsd
- members
meta:
$ref: '#/components/schemas/Meta'
warnings:
type: array
items:
$ref: '#/components/schemas/Warning'
Warning:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
Problem:
type: object
description: An RFC 9457 problem document. Served as `application/problem+json` on every failure except 402, which carries the x402 payment offer instead.
required:
- type
- title
- status
- code
- detail
properties:
type:
type: string
format: uri
description: A stable URI for this kind of failure.
title:
type: string
description: What went wrong, in a sentence that does not change per request.
status:
type: integer
minimum: 400
maximum: 599
description: The HTTP status, repeated here so the body stands alone.
code:
type: string
description: The machine token to switch on, e.g. invalid_parameter, not_found, rate_limited.
detail:
type: string
description: What went wrong with THIS request, and where possible what to do instead.
instance:
type: string
description: The path that produced it.
request_id:
type: string
description: Also sent as X-Request-Id. Quote it and we can find the request in our logs.
invalid_params:
type: array
description: Present on 400. Every parameter at fault, not only the first.
items:
type: object
required:
- name
- reason
properties:
name:
type: string
value:
type: string
description: The value we could not use, echoed back.
reason:
type: string
PaymentRequired:
type: object
required:
- error
- capability
- tier
- reason
description: 'Not an RFC 9457 problem document. The request was well formed and the answer is a price, so the body is an offer: what is missing, why, and where to settle it.'
properties:
forSale:
type: boolean
description: 'Whether this capability can be bought at all right now. False means the endpoint is real, the refusal is correct, and no plan on sale carries it: a product decision rather than something you have not bought yet. An agent should stop rather than retry. The message says the same in words.'
perCall:
type:
- object
- 'null'
description: 'The same call bought one at a time over x402, where this route is sold that way — null where it is not. Carries the full offer: `accepts` with the amount, asset, CAIP-2 network and address to pay, plus `priceUsd`. Present BESIDE `eligiblePlans` rather than instead of it: an agent cannot buy a monthly plan (it needs a person, a card and a key) and a person evaluating the platform should not be sold a tenth of a dollar without learning a plan exists. Neither reader is steered past the road that suits them.'
properties:
x402Version:
type: integer
accepts:
type: array
items:
type: object
priceUsd:
type: number
x-unit: usd
says:
type: string
description: When a plan is the cheaper road, in words.
eligiblePlans:
type: array
description: Plans on sale that carry the missing capability, cheapest first, each with a checkout URL. An empty array means no plan carries it — a capability we do not sell rather than one you have not bought.
items:
type: object
properties:
slug:
type: string
name:
type: string
monthlyUsd:
type: number
x-unit: usd
annualUsd:
type: number
x-unit: usd
quotaPerMonth:
type:
- integer
- 'null'
description: Requests a month, or null where the plan is unmetered.
x-unit: count
checkoutUrl:
type: string
checkoutUrl:
type: string
description: Where a person goes. Agents should read eligiblePlans.
error:
type: string
enum:
- payment_required
message:
type: string
description: Prose, for a person. Branch on `capability` and `reason`.
capability:
type: string
description: The capability the caller lacks, e.g. `cascade.read`. This is the field that says what to buy.
tier:
type: string
description: The tier the caller resolved to when the refusal was decided.
reason:
type: string
enum:
- tier
- arrears
- quota
description: tier — the plan does not carry it. arrears — it does, and something is owed, so the paid surface is closed until settled. quota — the monthly allowance is spent. Only the first is a reason to buy anything.
network:
type: string
description: Which chain a settlement would be read on.
accepts:
type: array
description: Per-request payment offers, where any are open. Empty means this capability is sold by plan rather than by the call.
items:
type: object
settlement:
type: object
description: How to pay, and what is missing if the caller cannot yet.
properties:
supported:
type: string
note:
type: string
address:
type:
- string
- 'null'
docs:
type: string
format: uri
Page:
type: object
required:
- total
- limit
- offset
- hasMore
properties:
total:
type: integer
minimum: 0
description: Rows matching the filter, not rows returned.
x-unit: count
limit:
type: integer
minimum: 0
description: Applied page size. May be below what was asked — X-Page-Limit says so when it is.
x-unit: count
offset:
type: integer
minimum: 0
x-unit: count
hasMore:
type: boolean
nextOffset:
type:
- integer
- 'null'
description: Null on the last page.
x-unit: count
orderedBy:
type: string
description: The sort, including its tie-breaker. The order is total, so paging cannot show a row twice or skip one.
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-API-Key
description: Issued from the account page.
payment:
type: apiKey
in: header
name: X-PAYMENT
description: 'Settle in-request: `<chain>:<txHash>` for a transfer already sent. Read on chain during the call and credited immediately.'
x-aspern-environment: production