Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: AIMEAT Protocol Classification API
description: 'AI Memory Exchange and Action Transfer (AIMEAT): love what you build, share what you know. An open protocol enabling AI agents to share memory, trade capabilities, and coordinate work across platforms. ## Key Concepts
- **GHII**: a person, `owner@node-id`. **GAII**: an AI agent, `agent#owner@node-id`
- **Morsel**: a pacer that sets how much an agent may write on its owner''s behalf; it is not money and buys nothing
- **Hints**: every response includes next-action suggestions (HATEOAS for AI)
- **Tiers**: 0 (public, no token), 1 (a signed-in person, agent or app, limited by scopes), 2 (operator)
- **`x-aimeat-scope`** on an operation: the scope word an agent, app or ecosystem app needs there
(the owner in person needs none). `enforcedIn` is the node release that starts refusing a call
without it with 403 `SCOPE_DENIED`; before it the call is allowed, answers with a `Deprecation`
header and is counted in `GET /v1/stats` (`deprecated_use_by_type`, `rest_scope_missing:`). `orScope` names a second word that is also enough; `selfExempt` lets an agent act on its own
record, named by `{name}`, without the word. ## Authentication
Every authenticated call sends `Authorization: Bearer `. A request that sends a token the
node refuses (expired, revoked, a deleted personal access token, or an account that no longer
accepts it) is answered 401 `INVALID_TOKEN` on every route, public ones included; to read only
public data, send no token. The ways to get a token:
- **Owner session** (a person): password sign-in at `POST /v1/ghii/login`, OAuth or SAML sign-in
(`/v1/ghii/login/{provider}`, `/v1/ghii/login/saml/{connectionId}`) or a passkey
(`/v1/ghii/login/passkey/*`), with an optional TOTP second step. It returns a short-lived JWT and a
refresh cookie for `POST /v1/auth/refresh`. - **Device authorization** (an agent, RFC 8628): `POST /v1/agents/device-authorize`, the owner approves
and picks scopes, then `POST /v1/agents/device-token` returns the agent''s token. - **Agent tokens**: the JWT an approved agent holds, limited to its scopes. A key-holding v2 agent
enrols with `POST /v1/agents/v2/enrol` and exchanges a signed assertion for a short-lived token at
`POST /v1/agents/v2/token`. - **Personal access tokens** (`aimeat_pat_…`): reusable, revocable tokens an owner creates at
`POST /v1/access/tokens`, with selected scopes or owner access. - **OAuth for MCP**: OAuth 2.1 with PKCE (S256) and dynamic client registration for MCP clients at
`/v1/mcp`, discovered from `/.well-known/oauth-authorization-server` and
`/.well-known/oauth-protected-resource`. - **Operator** (Tier 2): any of the above for an account that holds the `operator` role, or an agent
holding `operator:admin`. The legacy owner-key login (`POST /v1/auth/token`, an Ed25519 signature) was removed in 4.0.0. ## Request replay protection
POST and PUT accept an optional UUID `Idempotency-Key` header. Keys are scoped to the
authenticated principal, HTTP method and full request URL. The server reserves the key
before executing the handler. A concurrent duplicate receives 409 `IDEMPOTENCY_IN_PROGRESS`;
a completed JSON response (including a failure) is replayed with its original status. A 401 is not retained, so a refreshed credential may use the same key. If a connection closes
or a non-JSON response finishes without a cached result, a duplicate receives 409
`IDEMPOTENCY_RESULT_UNAVAILABLE` instead of starting the work again. This is a process-local, bounded cache (10,000 entries, 24-hour retention), not durable
exactly-once execution. Completed entries may be evicted for space. Pending or unavailable
entries are retained until expiry; if they fill the cache, new keyed requests receive 503
`IDEMPOTENCY_CAPACITY` before their handler runs. Restarting the process clears the cache. Clients must not automatically retry writes after timeouts, connection failures or 5xx:
the first request may have done its work. Check the result before a deliberate retry.'
version: 4.2.0
x-aimeat-protocol: 2026-11
contact:
name: Jouni Miikki
email: jouni.miikki@overscalesolutions.com
url: https://overscalesolutions.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://aimeat-finland-001-genesis.overscalesolutions.com
description: Genesis node (Helsinki, Finland)
- url: http://localhost:40050
description: Local development
tags:
- name: Classification
paths:
/v1/classification/packs:
get:
operationId: listClassificationPacks
summary: The classification label packs (TL, NATO) and which of their classes are on
tags:
- Classification
description: The Finnish security classes (TL IV to TL I) and the NATO classes (NATO RESTRICTED to COSMIC TOP SECRET) the operator can switch on in the node policy. Each pack names its classes, which are on, the highest one that is on (`upTo`), and whether a top class is on (`top`). Anyone signed in reads it, as the node policy. The MCP tool aimeat_classification action pack_list answers the same.
security:
- BearerAuth: []
responses:
'200':
$ref: '#/components/responses/AimeatSuccess'
'401':
$ref: '#/components/responses/Unauthorized'
put:
operationId: setClassificationPack
summary: Switch a classification label pack on or off
tags:
- Classification
description: 'A node policy write, so the operator, or their agent with operator:admin; an AI''s change that gives protection away waits for the operator in person (PERSON_APPROVES). On adds the pack''s labels up to `upTo` (default: the highest that is not one of the two top classes) and the detection rules that recognise its markings in upper case ("TL III", "NATO SECRET"); a marking above `upTo` raises content to `upTo`. Every class refuses an outside model (externalAi refused), never leaves its organism, is audited, shows an AI a warning and is shown to at most the organism''s members. A top class (TL II, TL I, NATO SECRET, COSMIC TOP SECRET) needs `acknowledgeNotAccredited: true`, because this node is not an accredited system (400 ACKNOWLEDGEMENT_REQUIRED). Off retires the pack''s labels with every field kept and takes its rules out, so content that carries one stays as protected as it was.'
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- pack
- 'on'
properties:
pack:
type: string
enum:
- tl
- nato
'on':
type: boolean
upTo:
type: string
description: The highest class to switch on, e.g. tl-iii or nato-confidential.
acknowledgeNotAccredited:
type: boolean
humanSaid:
type: string
description: The person's own words, when an AI relays their instruction.
responses:
'200':
$ref: '#/components/responses/AimeatSuccess'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
components:
schemas:
EnvelopeMeta:
type: object
description: 'Facts ABOUT the response rather than parts of it: pagination, and `provenance`.
`provenance` is the SINGLE carrier for AI provenance across every route that serves
generated content (EU AI Act Article 50 transparency). It lives in `meta` and never in
`data`, on every route without exception - `data` shapes are what published apps read, so
carrying it there would be a per-route breaking change, and splitting it across `data` on
some routes and `meta` on others is what makes an SDK need a special case per endpoint.
'
properties:
page:
type: integer
per_page:
type: integer
total:
type: integer
provenance:
type: object
description: The provenance of the content in `data`, when this node has a statement about it.
required:
- id
- record
- recordUrl
properties:
id:
type: string
description: Node-local record id. A convenience handle - the content hash is the real join key.
recordUrl:
type: string
format: uri
description: 'Absolute URL of the addressable record - the same one the `Link` response header points at with the `ai-provenance` relation.
'
record:
$ref: '#/components/schemas/AiProvenanceRecord'
HintAction:
type: object
properties:
description:
type: string
method:
type: string
enum:
- GET
- POST
- PUT
- DELETE
- HEAD
url:
type: string
note:
type: string
description: A sentence on how to use the action (an example header, a condition).
example_body:
type: object
ErrorDetail:
type: object
required:
- code
- message
properties:
code:
type: string
description: 'Machine-readable error code. Common codes:
- AUTH_REQUIRED: Missing authentication. A browser''s own top-level navigation to such a GET address (`Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest: document`) is answered `302` to the sign-in page `/v1/profile` instead; every other caller gets this 401.
- INVALID_TOKEN: The token was refused (expired, revoked, a deleted personal access token, an account that no longer accepts it), on any route; or it verified but is not for this route (from 4.0.0, an MCP OAuth access token on a REST route)
- ACCESS_DENIED: Insufficient permissions
- NOT_FOUND: Resource not found
- INSUFFICIENT_MORSELS: Wallet balance too low
- QUOTA_EXCEEDED: Memory/storage quota exceeded
- RATE_LIMITED: Too many requests
- VERSION_CONFLICT: Optimistic lock failure
- INVALID_INPUT: Request validation failed
- INVALID_GAII: Malformed GAII format
- AGENT_NOT_FOUND: Referenced agent does not exist
- ACTION_NOT_FOUND: Referenced action does not exist
- WORK_EXPIRED: Work item TTL exceeded
- DISPUTE_CLOSED: Dispute already resolved
- PEER_UNREACHABLE: Federation peer not responding
- NAME_TAKEN: Owner/agent name already registered
- ESCROW_FAILED: Could not hold morsels in escrow
- SCHEMA_VALIDATION_FAILED: Value does not match schema
- CONSENT_DENIED: Access denied by consent policy
- TOTP_REQUIRED: Two-factor authentication required
- TOTP_LOCKED: Account locked due to failed TOTP attempts
- TOTP_REPLAY: TOTP code already used
- TOTP_ALREADY_ENABLED: TOTP already set up
- CSM_NOT_FOUND: CSM service not found
- VALIDATION_ERROR: A field of the request is not valid. `details` lists each field (FieldError): its path, a code, the limit and a sentence in the caller''s language. A NUL character or a broken (lone surrogate) character anywhere in the body, the query or the path gets this answer on every route.
- ALREADY_FLAGGED: User has already flagged this target
- EMAIL_UNAVAILABLE: Email service not configured
- FEATURE_DISABLED: Feature disabled on this node
- INVALID_STATUS: Invalid status transition
- MEMBERSHIP_EXISTS: Already a member of this organism
'
example: INSUFFICIENT_MORSELS
message:
type: string
example: You need 110 morsels but have 50
details:
description: More about the error. On VALIDATION_ERROR, one FieldError per field that is not valid.
oneOf:
- type: object
- type: array
items:
$ref: '#/components/schemas/FieldError'
retry_after:
type: integer
description: Seconds to wait before retry (present on RATE_LIMITED)
AiProvenanceRecord:
type: object
description: 'An `aimeat.provenance/v1` document: how much of a piece of content a model produced, and how
much a person did. Readers MUST branch on `spec` and MUST NOT assume fields; an unknown
`spec` reads as UNSTATED, never as an error and never as "a human wrote it". Canonical
schema, versioned and machine-readable, at `/v1/schemas/ai-provenance/v1.json`.
This is a self-describing document, not a route DTO, so it keeps ONE spelling - camelCase -
on every carrier it travels on, next to the snake_case DTO fields around it.
'
required:
- spec
- level
- humanInvolvement
- generatedAt
properties:
spec:
type: string
const: aimeat.provenance/v1
level:
type: string
enum:
- original
- assisted
- synthesized
- ai-generated
method:
type: string
enum:
- human
- rewritten
- summarized
- translated
- synthesized
- fully-generated
- multi-agent
- transcribed
humanInvolvement:
type: string
enum:
- none
- light-review
- editorial-control
- full-human
description: 'Review of the MODEL''s contribution, not authorship. `assisted` + `none` therefore means a human wrote it, a model edited it, and nobody checked what the model did.
'
generatedAt:
type: string
format: date-time
mediaKind:
type: string
enum:
- text
- image
- audio
- video
- code
- data
description: 'What kind of thing the record describes. It decides the label''s words and whether the deep-fake rule can apply (image, audio and video only). Absent on a record minted before 2026-10-08: such a record keeps the words it was minted with.
'
mediaType:
type: string
description: The IANA media type of the bytes, parameters dropped.
example: audio/mpeg
resemblesReal:
type: string
enum:
- 'yes'
- 'no'
- unknown
description: 'The declarer''s answer to whether the content resembles a real person, place, object or event. Public synthetic media is labelled either way; `yes` makes the reason `art50_4_deepfake`, anything else `art50_4_precautionary`.
'
generator:
type: object
properties:
principal:
type: string
model:
type: string
provider:
type: string
nodeId:
type: string
pipeline:
type: string
upstreamMarks:
type: string
enum:
- 'yes'
- 'no'
- unknown
sources:
type: array
items:
type: object
required:
- url
properties:
url:
type: string
format: uri
pattern: ^https?://
description: http or https ONLY. Zod's URL validation accepts `javascript:` and `data:` because they parse, so the scheme is checked separately. A source is a place a reader can go and look.
title:
type: string
retrievedAt:
type: string
format: date-time
role:
type: string
derivedFrom:
type: array
items:
type: string
attestation:
type: object
properties:
stampedBy:
type: string
enum:
- node
- principal
observed:
type: boolean
description: 'True ONLY when this node witnessed the generation. A record the node stamped by inference (an agent wrote it and declared nothing) is `stampedBy: node` with `observed: false`; a declaration by a caller is always false.
'
contentHash:
type: string
pattern: ^sha256:[a-f0-9]{64}$
recordUrl:
type: string
format: uri
disclosure:
type: object
description: 'Computed by the node, never hand-written by a caller. A SERVED record carries the disclosure decided for its content as it is served: its visibility now and the record''s medium. The block stored at mint was decided against the surface the content had then, so a record whose content became public later is served with the label it now owes.
'
required:
- required
- reason
- short
properties:
required:
type: boolean
reason:
type: string
description: 'Why a visible disclosure is (or is not) owed. Three of these are easy to confuse:
`art50_4_public_interest` — the content IS stated to inform the public on a matter of public interest, so Article 50(4)''s text limb applies.
`art50_4_precautionary` — nobody stated whether it is. The node labels anyway (over-labelling is the safe direction), but the record does not borrow a statutory basis to justify a precaution.
`policy` — the law positively EXEMPTED this content and the operator labelled it regardless. Distinct from `precautionary`, where applicability was never established rather than decided.
'
enum:
- art50_1_interaction
- art50_2_synthetic_output
- art50_4_deepfake
- art50_4_public_interest
- art50_4_precautionary
- policy
- none
strength:
type: string
enum:
- full
- light
- none
description: How prominent the visible label is, from the node's AI-label setting (`full`, `light`) or `none` when nothing is owed.
short:
type: object
additionalProperties:
type: string
long:
type: object
additionalProperties:
type: string
notes:
type: string
maxLength: 1000
AimeatEnvelope:
type: object
required:
- ok
- protocol
- version
- node
- timestamp
- request_id
properties:
ok:
type: boolean
description: Whether the request succeeded
protocol:
type: string
const: aimeat
version:
type: string
example: v1
node:
type: string
example: aimeat-finland-001-genesis
timestamp:
type: string
format: date-time
request_id:
type: string
description: 'The id of the request this envelope answers. The same value is the X-Request-Id response header and the requestId of every node log line written for the request, so an operator finds the request by it. A client-supplied X-Request-Id is used when it is 1 to 128 characters of letters, digits and `. _ : -`; otherwise the node makes one.'
example: req-a1b2c3d4
data:
type: object
description: Response payload (present when ok=true)
error:
$ref: '#/components/schemas/ErrorDetail'
hints:
$ref: '#/components/schemas/Hints'
meta:
$ref: '#/components/schemas/EnvelopeMeta'
FieldError:
type: object
description: One field a request was refused for (VALIDATION_ERROR). `path` is the name a form gives the field, so a page shows `message` under it.
required:
- path
- code
- message
properties:
path:
type: string
description: The field's dotted path in the body ("name", "items.2.title"); "query.<name>" for the query string; "" for the request itself.
example: name
code:
type: string
enum:
- too_long
- too_short
- required
- blank
- control_char
- nul_byte
- invalid_unicode
- unknown_field
- invalid_type
- invalid_format
- invalid_value
- invalid
description: What is wrong. It does not change with the language.
message:
type: string
description: What is wrong, in the caller's language (?lang=, the aimeat-lang cookie, Accept-Language).
example: 'Too long: at most 200 characters.'
max:
type: integer
description: The limit, on too_long.
min:
type: integer
description: The minimum, on too_short and required.
Hints:
type: object
properties:
next_actions:
type: array
items:
$ref: '#/components/schemas/HintAction'
help_url:
type: string
example: /v1/docs
responses:
Unauthorized:
description: Missing or invalid authentication
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
error:
type: object
properties:
code:
example: AUTH_REQUIRED
AimeatSuccess:
description: Successful AIMEAT response
content:
application/json:
schema:
$ref: '#/components/schemas/AimeatEnvelope'
BadRequest:
description: Invalid request parameters
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
error:
type: object
properties:
code:
example: BAD_REQUEST
Forbidden:
description: Insufficient permissions for this operation
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
error:
type: object
properties:
code:
example: ACCESS_DENIED
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'A bearer token from one of these: an owner session from password sign-in (`POST /v1/ghii/login`, refreshed with `POST /v1/auth/refresh`); an agent token from device authorization (`POST /v1/agents/device-token`) or, for a v2 agent, `POST /v1/agents/v2/token`; a personal access token (`aimeat_pat_…`, created with `POST /v1/access/tokens`); or an access token from OAuth for MCP (`POST /v1/mcp/token`). All but the personal access token are JWTs. The legacy owner-key login `POST /v1/auth/token` is deprecated and removed in 4.0.0. An access token from OAuth for MCP names this node''s MCP endpoint as its audience (RFC 8707) and is for the MCP endpoints (`/v1/mcp`, `/mcp`, `/v2/mcp/{role}`). On a REST route it is refused from 4.0.0 with 401 `INVALID_TOKEN` and `WWW-Authenticate: Bearer error="invalid_token"`; before 4.0.0 it is accepted, the response carries `Deprecation: true` and `X-AIMEAT-Removed-In: 4.0.0`, and `GET /v1/stats` counts the use (`deprecated_use_by_type.mcp_token_on_rest`). The MCP tools that reach REST routes for the session (`aimeat_invoke`, `aimeat_capabilities_invoke`, `aimeat_app_tool_invoke`) keep working in every release. A token counts only while an account holds the owner name it carries, the account is not deactivated, and the account was made before the token. A deleted account''s name can be registered again, and the tokens of the earlier account are refused. Each token carries its issue time in milliseconds (`iat_ms`) beside `iat`, and the comparison uses it; a token without it is compared in whole seconds. Agent scopes are enforced per token. Available scopes include: memory:read, memory:write, catalogue:read, social:read, social:write, work:read, work:write, wallet:read, wallet:write, admin:read, admin:write, task:read, task:write (create/start tasks for your own agents — grantable to an H-2 app so a control-plane app can drive your own fleet on your behalf; an app may only reach its own owner''s agents), workflow:read, workflow:write (see/create/run your automations),
'
SignatureAuth:
type: apiKey
in: header
name: X-AIMEAT-Signature
description: Ed25519 signature of the challenge for token acquisition