Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: AIMEAT Protocol Schema 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: Schema
description: Phase 0.1 — Schema locking for memory key patterns
paths:
/v1/memory/{key}/schema:
put:
operationId: setMemorySchema
summary: Set schema for a memory key pattern
description: 'Lock a memory key (or key prefix) to a JSON Schema. All future writes
to matching keys, by every account, are validated against this schema.
An account holder or the operator may set one, within these limits
(services/schema-lock-rule.ts):
- a pattern that starts with a wildcard, or a trailing-star pattern that
is not a `name:*` key family, reaches every account''s keys: operator only
(403 SCHEMA_PATTERN_NODE_WIDE);
- a pattern under `organism..` is set by the organism''s admins, the
workspace''s creator, or the member whose namespace it is
(403 SCHEMA_PATTERN_NOT_YOURS); a wildcard organism id is operator only;
- the same pattern locked by someone else, under either apply_to, or a key
another account''s lock already governs, is refused
(403 SCHEMA_LOCKED_BY_OTHER) unless the operator overrides it.'
tags:
- Schema
security:
- BearerAuth: []
parameters:
- name: key
in: path
required: true
schema:
type: string
description: Memory key or key prefix to lock
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SchemaLockRequest'
responses:
'200':
description: Schema set successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
data:
type: object
properties:
key:
type: string
apply_to:
type: string
schema_mode:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
description: Invalid JSON Schema
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
error:
type: object
properties:
code:
example: VALIDATION_ERROR
get:
operationId: getMemorySchema
summary: Get schema for a memory key
description: Retrieve the JSON Schema locked to a specific memory key or prefix. No authentication required.
tags:
- Schema
parameters:
- name: key
in: path
required: true
schema:
type: string
description: Memory key to look up
responses:
'200':
description: Schema for the key
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
data:
type: object
properties:
key:
type: string
apply_to:
type: string
schema_mode:
type: string
schema:
type: object
semantic_context:
$ref: '#/components/schemas/SemanticAnnotation'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteMemorySchema
summary: Remove schema from a memory key
description: 'Remove one schema lock row: the (key pattern, apply_to) pair. Only the
principal that set it, or the operator, may remove it (403 NOT_SCHEMA_OWNER).
Without apply_to the exact lock is the row, else the prefix lock; the other
row of the same pattern, which may be another principal''s, is left in place.'
tags:
- Schema
security:
- BearerAuth: []
parameters:
- name: key
in: path
required: true
schema:
type: string
description: Memory key to unlock
- name: apply_to
in: query
required: false
schema:
type: string
enum:
- exact
- prefix
description: Which row of the pattern to remove. Leave out for the exact row, or the prefix row when there is no exact one.
responses:
'200':
$ref: '#/components/responses/AimeatSuccess'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/v1/schemas:
get:
operationId: listSchemas
summary: List all schemas
description: 'List all registered schema locks on this node, or those whose key pattern starts with
`prefix`. No authentication required. The list is not paged: every matching lock is in one
answer, with `total` its length. The schema body and its semantic context are read per lock
with GET /v1/memory/{key}/schema.'
tags:
- Schema
parameters:
- name: prefix
in: query
required: false
schema:
type: string
description: Only the locks whose key pattern starts with this text.
responses:
'200':
description: 'The schema locks: { schemas, total }'
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
data:
type: object
properties:
schemas:
type: array
items:
type: object
properties:
key:
type: string
description: The key pattern the lock applies to.
apply_to:
type: string
schema_mode:
type: string
locked_by:
type: string
set_at:
type: string
format: date-time
has_semantic:
type: boolean
description: Whether the lock carries a semantic context.
total:
type: integer
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)
SemanticAnnotation:
type: object
description: 'A JSON-LD annotation. Every prefix it names must resolve: either declared in its own `@context`, or one this node carries by default (aimeat, schema, skos, dcterms, prov, rdfs, qudt, saref — see GET /v1/ns). A prefixed name whose prefix nobody defined is refused, since it expands to nothing in any JSON-LD processor. A BARE type (`"@type": "LocalBusiness"`) is always accepted: it resolves against the document''s default vocabulary. Ontology-specific properties pass through untouched, and are checked the same way.'
properties:
'@context':
type: object
additionalProperties:
type: string
'@type':
type: string
additionalProperties: true
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
SchemaLockRequest:
type: object
required:
- schema
- apply_to
properties:
schema:
type: object
description: JSON Schema to enforce on matching keys
apply_to:
type: string
enum:
- exact
- prefix
schema_mode:
type: string
enum:
- open
- strict
default: open
semantic_context:
allOf:
- $ref: '#/components/schemas/SemanticAnnotation'
description: What the data under this key MEANS, beside the schema that says what shape it is. Its optional `properties` map takes a FIELD of the schema to the ontology term for that field. A field name in there that the schema does not have is refused with INVALID_SEMANTIC_CONTEXT, because a mapping pointing at no field reads as complete and describes nothing.
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'
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
NotFound:
description: Resource not found
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/AimeatEnvelope'
- type: object
properties:
error:
type: object
properties:
code:
example: NOT_FOUND
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