{"id":"9d8ae8ecbd7a2c792671bcd6bbc9264a02b0903ca52e24d781d9e39a653e4a64","pubkey":"bdc21f93b1e2cb75608cecd7a0a00a779779d9367dc9798bd9f213f06c95bc48","created_at":1787819077,"kind":30817,"tags":[["d","edufeed-amb"],["title","NIP-AMB"],["k","30142","AMB Metadata Event"]],"content":"# NIP-AMB\n\n## Abstract\n\nThis NIP defines how to handle the metadata profile [\"Allgemeines Metadatenprofil für Bildungsressourcen\" (AMB)](https://dini-ag-kim.github.io/amb/latest/) in nostr:\n\n- How to convert AMB metadata to an AMB nostr event\n- How to convert an AMB nostr-event to AMB metadata\n- How to query for AMB nostr-events in supporting relays\n\n## Event Kind\n\nThis NIP defines `kind:30142` as an AMB Metadata Event.\nThis means this is an addressable event, that can be addressed using `kind:pubkey:d-tag`.\n\n## How to convert AMB metadata *to* an AMB nostr event\n\nThe transformation uses JSON-flattening with `:` as the delimiter to convert nested AMB metadata structures into flat Nostr tags. Additionally, Nostr-native tag conventions are used where applicable for better interoperability and query efficiency.\n\n### Nostr-Native Conventions\n\nThis NIP follows Nostr conventions where they align with AMB requirements:\n\n- **`d` tag**: Used as the unique identifier for the AMB resource (maps to AMB `id`)\n- **`t` tags**: Used for keywords/topics (instead of flattened `keywords` tags)\n- **`p` tags**: Used for creator/contributor references when the person has a Nostr identity (pubkey). Format: `[\"p\", <pubkey-hex>, <relay-hint>, <role>]` where `<role>` is `\"creator\"` or `\"contributor\"`. The relay hint is a single suggestion for discovery (per NIP-01 convention); clients SHOULD use NIP-65 for full relay resolution. When a `p` tag is used for a person, no `creator:*`/`contributor:*` flattened tags are emitted for that person — the pubkey IS their identity. Persons without a Nostr identity use the flattened `creator:*`/`contributor:*` tag structure instead.\n- **`a` tags**: Used for references to other addressable events on Nostr (including other AMB events), with fallback to flattened URIs for external resources. Format: `[\"a\", \"30142:<pubkey>:<d-value>\", <relay-hint>, <relationship>]`\n- **`r` tags**: Used for external URL references (original source, DOI, related web resources)\n- **`content` field**: SHOULD contain the description text for client compatibility; the `description` tag is kept for relay queryability\n\n### Flattening Rules\n\n1. **Simple properties**: Map directly to `[\"<key>\", \"<value>\"]` tags\n   - AMB: `{\"name\": \"Resource Title\"}`\n   - Nostr: `[\"name\", \"Resource Title\"]`\n\n2. **Nested objects**: Flatten using `:` delimiter\n   - AMB: `{\"creator\": {\"name\": \"John\", \"id\": \"123\"}}`\n   - Nostr: `[\"creator:name\", \"John\"]`, `[\"creator:id\", \"123\"]`\n\n3. **Arrays**: Repeat the same flattened tag key (order is preserved by tag array position)\n   - AMB: `{\"keywords\": [\"Math\", \"Physics\"]}`\n   - Nostr: `[\"t\", \"Math\"]`, `[\"t\", \"Physics\"]` (Nostr-native `t` tag)\n\n4. **Arrays of objects**: Repeat flattened keys for each object\n   - AMB: `{\"creator\": [{\"name\": \"John\"}, {\"name\": \"Jane\"}]}`\n   - Nostr: `[\"creator:name\", \"John\"]`, `[\"creator:name\", \"Jane\"]`\n\n5. **Deep nesting**: Continue flattening with additional `:` delimiters\n   - AMB: `{\"creator\": {\"affiliation\": {\"name\": \"MIT\"}}}`\n   - Nostr: `[\"creator:affiliation:name\", \"MIT\"]`\n\n### Property Mappings\n\nThis is how we convert each property of the AMB:\n\n#### General:\n\n- `id` → `[\"d\", <id>]` (special case: use Nostr's `d` tag as identifier). The `d` value SHOULD be the resource's canonical, dereferenceable URL when one exists (the AMB spec describes `id` as a dereferenceable HTTP URI). Nostr-native resources without an external URL MAY use an arbitrary stable slug — see the reverse-conversion rules for how the AMB `id` is derived in that case.\n- `type` → `[\"type\", <value>]` (repeat for multiple types)\n- `name` → `[\"name\", <value>]`\n- `description` → `[\"description\", <value>]` AND `\"content\": <value>` (duplicated for client compatibility and relay queryability)\n- `about` (array of concept objects) → Repeat for each:\n  - `[\"about:id\", <uri>]`\n  - `[\"about:prefLabel:lang\", <label>]`\n  - `[\"about:type\", \"Concept\"]`\n- `keywords` → `[\"t\", <keyword>]` (repeat for each keyword, using Nostr `t` tag)\n- `inLanguage` → `[\"inLanguage\", <languageCode>]` (repeat for each language)\n- `image` → `[\"image\", <uri>]`\n- `trailer` (MediaObject) →\n  - `[\"trailer:contentUrl\", <url>]`\n  - `[\"trailer:type\", <\"VideoObject\"|\"AudioObject\">]`\n  - `[\"trailer:encodingFormat\", <format>]` (optional)\n  - `[\"trailer:contentSize\", <bytes>]` (optional)\n  - `[\"trailer:sha256\", <hash>]` (optional)\n  - `[\"trailer:embedUrl\", <url>]` (optional)\n  - `[\"trailer:bitrate\", <kbps>]` (optional)\n\n#### Provenance:\n\n- `creator` (array of Person/Organization objects) → For each creator, use **one** of the following (never both for the same person):\n  - **Nostr-native (creator has a Nostr pubkey)**: `[\"p\", <pubkey-hex>, <relay-hint>, \"creator\"]` — no additional `creator:*` tags for this person. Their name and metadata are resolved from their kind:0 profile.\n    - **Detection**: If a creator object's `id` is a `nostr:` URI per [NIP-21](https://github.com/nostr-protocol/nips/blob/master/21.md) encoding an `npub` or `nprofile`, converters MUST decode it and emit the `p` tag form instead of flattened `creator:*` tags. (This is valid AMB input: the AMB schema constrains `creator.id` only to `format: uri`; the ORCID/GND/Wikidata/ROR list is a SHOULD-level recommendation.)\n    - **Relay hint precedence**: relay embedded in the `nprofile` (first entry) → converter-configured default → empty string.\n  - **External (no Nostr identity)**:\n    - `[\"creator:id\", <uri>]` (optional, e.g., ORCID, GND)\n    - `[\"creator:name\", <name>]`\n    - `[\"creator:type\", <\"Person\"|\"Organization\">]`\n    - `[\"creator:honorificPrefix\", <title>]` (optional, for persons)\n    - `[\"creator:affiliation:id\", <uri>]` (optional)\n    - `[\"creator:affiliation:name\", <name>]` (optional)\n    - `[\"creator:affiliation:type\", \"Organization\"]` (optional)\n- `contributor` (array of Person/Organization objects) → Same structure as `creator` (including `nostr:` URI detection), using role `\"contributor\"` in the `p` tag\n\n  > **Known limitation:** A person with both a Nostr identity and external identifiers (e.g., an ORCID) is represented by the `p` tag alone; their external identifier, `affiliation`, and `honorificPrefix` are not carried in the event, since kind:0 profiles have no standard fields for them. This deviates from the AMB SHOULD-level recommendation to reference ORCID/GND/Wikidata/ROR and is accepted as a trade-off for having exactly one unambiguous representation per person.\n\n- `dateCreated` → `[\"dateCreated\", <ISO8601Date>]`\n- `datePublished` → `[\"datePublished\", <ISO8601Date>]`\n- `dateModified` → `[\"dateModified\", <ISO8601Date>]`\n- `publisher` (array of Organization/Person objects) → Repeat for each:\n  - `[\"publisher:id\", <uri>]` (optional)\n  - `[\"publisher:name\", <name>]`\n  - `[\"publisher:type\", <\"Organization\"|\"Person\">]`\n- `funder` (array of Person/Organization/FundingScheme objects) → Repeat for each:\n  - `[\"funder:id\", <uri>]` (optional)\n  - `[\"funder:name\", <name>]`\n  - `[\"funder:type\", <\"Person\"|\"Organization\"|\"FundingScheme\">]`\n\n> **Note:** No `p`-tag role is defined for `publisher` or `funder`. A `nostr:` URI in their `id` is emitted verbatim as the flattened `publisher:id`/`funder:id` value.\n\n#### Costs and Rights:\n\n- `isAccessibleForFree` → `[\"isAccessibleForFree\", <\"true\"|\"false\">]`\n- `license` (object) →\n  - `[\"license:id\", <license_uri>]`\n- `conditionsOfAccess` (Concept object) →\n  - `[\"conditionsOfAccess:id\", <uri>]`\n  - `[\"conditionsOfAccess:prefLabel:lang\", <label>]` (optional)\n  - `[\"conditionsOfAccess:type\", \"Concept\"]` (optional)\n\n#### Educational:\n\n- `learningResourceType` (array of Concept objects) → Repeat for each:\n  - `[\"learningResourceType:id\", <uri>]`\n  - `[\"learningResourceType:prefLabel:lang\", <label>]` (optional)\n  - `[\"learningResourceType:type\", \"Concept\"]` (optional)\n- `audience` (array of Concept objects) → Repeat for each:\n  - `[\"audience:id\", <uri>]`\n  - `[\"audience:prefLabel:lang\", <label>]` (optional)\n  - `[\"audience:type\", \"Concept\"]` (optional)\n- `teaches` (array of Concept objects) → Repeat for each:\n  - `[\"teaches:id\", <uri>]`\n  - `[\"teaches:prefLabel:lang\", <label>]` (optional)\n- `assesses` (array of Concept objects) → Repeat for each:\n  - `[\"assesses:id\", <uri>]`\n  - `[\"assesses:prefLabel:lang\", <label>]` (optional)\n- `competencyRequired` (array of Concept objects) → Repeat for each:\n  - `[\"competencyRequired:id\", <uri>]`\n  - `[\"competencyRequired:prefLabel:lang\", <label>]` (optional)\n- `educationalLevel` (array of Concept objects) → Repeat for each:\n  - `[\"educationalLevel:id\", <uri>]`\n  - `[\"educationalLevel:prefLabel:lang\", <label>]` (optional)\n  - `[\"educationalLevel:type\", \"Concept\"]` (optional)\n- `interactivityType` (Concept object) →\n  - `[\"interactivityType:id\", <uri>]`\n  - `[\"interactivityType:prefLabel:lang\", <label>]` (optional)\n  - `[\"interactivityType:type\", \"Concept\"]` (optional)\n- `suggestedAge` (object with integer bounds; AMB requires at least one of `minValue`/`maxValue`) →\n  - `[\"suggestedAge:minValue\", <integer>]` (optional)\n  - `[\"suggestedAge:maxValue\", <integer>]` (optional)\n\n#### Relations:\n\n- `isBasedOn` (array of objects) → Repeat for each:\n  - **Nostr-native (if referenced resource is addressable AMB event)**: `[\"a\", \"30142:<pubkey>:<d-value>\", <relay>, \"isBasedOn\"]`\n  - **Fallback (for external URIs)**:\n    - `[\"isBasedOn:id\", <uri>]` (omit when the relation has no `id` — AMB allows name-only `isBasedOn` references; never emit a literal `\"undefined\"`)\n    - `[\"isBasedOn:name\", <name>]` (optional)\n- `isPartOf` (array of objects) → Repeat for each:\n  - **Nostr-native (if referenced resource is addressable AMB event)**: `[\"a\", \"30142:<pubkey>:<d-value>\", <relay>, \"isPartOf\"]`\n  - **Fallback (for external URIs)**:\n    - `[\"isPartOf:id\", <uri>]`\n    - `[\"isPartOf:name\", <name>]` (optional)\n    - `[\"isPartOf:type\", <type>]` (optional)\n- `hasPart` (array of objects) → Repeat for each:\n  - **Nostr-native (if referenced resource is addressable AMB event)**: `[\"a\", \"30142:<pubkey>:<d-value>\", <relay>, \"hasPart\"]`\n  - **Fallback (for external URIs)**:\n    - `[\"hasPart:id\", <uri>]`\n    - `[\"hasPart:name\", <name>]` (optional)\n    - `[\"hasPart:type\", <type>]` (optional)\n\n#### Meta-Metadata:\n\n- `mainEntityOfPage` (array of WebPage objects) → Repeat for each:\n  - `[\"mainEntityOfPage:id\", <uri>]`\n  - `[\"mainEntityOfPage:type\", \"WebContent\"]`\n  - `[\"mainEntityOfPage:provider:id\", <uri>]` (optional)\n  - `[\"mainEntityOfPage:provider:name\", <name>]` (optional)\n  - `[\"mainEntityOfPage:provider:type\", <type>]` (optional)\n  - `[\"mainEntityOfPage:dateCreated\", <ISO8601Date>]` (optional)\n  - `[\"mainEntityOfPage:dateModified\", <ISO8601Date>]` (optional)\n\n#### Technical:\n\n- `duration` → `[\"duration\", <ISO8601Duration>]` (format: PnYnMnDTnHnMnS)\n- `encoding` (array of MediaObject objects) → Repeat for each:\n  - `[\"encoding:type\", \"MediaObject\"]`\n  - `[\"encoding:contentUrl\", <url>]` (or use `embedUrl`)\n  - `[\"encoding:embedUrl\", <url>]` (or use `contentUrl`)\n  - `[\"encoding:encodingFormat\", <format>]` (optional, IANA media type)\n  - `[\"encoding:contentSize\", <bytes>]` (optional)\n  - `[\"encoding:sha256\", <hash>]` (optional)\n  - `[\"encoding:bitrate\", <kbps>]` (optional)\n- `caption` (array of MediaObject objects) → Repeat for each:\n  - `[\"caption:id\", <uri>]`\n  - `[\"caption:type\", \"MediaObject\"]`\n  - `[\"caption:encodingFormat\", <format>]` (optional, IANA media type)\n  - `[\"caption:inLanguage\", <languageCode>]` (optional)\n\n#### External References:\n\nSupplementary \"see also\" references use the Nostr-native `r` tag (per NIP-24). These are Nostr-native metadata for client interoperability and do not map to a specific AMB property on reverse conversion.\n\n- `[\"r\", <url>]` - Repeat for each external reference\n\nExamples:\n- `[\"r\", \"https://oersi.org/resources/xyz\"]` - Original source URL\n- `[\"r\", \"https://doi.org/10.1234/example\"]` - DOI reference\n- `[\"r\", \"urn:isbn:978-3-16-148410-0\"]` - ISBN reference\n\n#### Extension Properties (ext namespace):\n\nProperties not standardized in AMB-core SHOULD use the `ext` namespace. The shape mirrors AMB-core's flattening, with one extra leading segment that identifies the publishing authority. This enables non-AMB-conformant metadata to coexist with AMB-core in a single event without collision risk.\n\n- Tag form: `[\"ext:<ns>:<facet>\", \"<value>\"]` (scalar) or `[\"ext:<ns>:<facet>:<sub>\", \"<value>\"]` (structured)\n  - `<ns>` — namespace authority slug. MUST NOT contain `:`. Lowercase; `.` and `-` are permitted. Stable per authority. Examples: `ekw`, `oersi`, `org.edufeed.ekw`.\n  - `<facet>` — field name within the namespace. MUST NOT contain `:`. Examples: `bistum`, `ressourcentyp`, `fach`.\n  - `<sub>` — property suffix, identical to AMB-core, drawn from a **closed set**: `id`, `type`, `name`, or `prefLabel:<lang>`. A key with no `<sub>` is a scalar (see below).\n- Example (single concept):\n  - `[\"ext:ekw:bistum:id\", \"https://w3id.org/kim/ekw/bistum/hannover\"]`\n  - `[\"ext:ekw:bistum:prefLabel:de\", \"Hannover\"]`\n  - `[\"ext:ekw:bistum:type\", \"Concept\"]`\n- Multiple values for the same `<ns>:<facet>` pair repeat the tag triple, exactly as AMB-core arrays do (boundary on repeated `id`).\n- Implementations MUST NOT fold ext entries into AMB-core properties on reverse conversion. They surface as a sibling `ext` object — see Example 3 and the reverse-conversion section.\n\n##### Namespace selection and collision avoidance\n\n`<ns>` is exactly **one** colon-free segment. Authorship, form identity, and deployment MUST NOT be encoded inside `<ns>`; a key such as `ext:30168:<pubkey>:<d-tag>:<facet>:id` is **invalid** under this NIP.\n\nThe rationale is the same as [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md), which keeps a namespace in a separate tag position rather than inside the tag name: a single-segment `<ns>` lets the same logical facet unify across authors, forks and deployments of the same vocabulary, which is what makes `#ext:<ns>:<facet>:id` a usable filter. Multi-segment namespaces are also not parseable without out-of-band knowledge — see the parsing rule below.\n\nTo avoid collisions without a central registry, authorities SHOULD use **reverse domain name notation**, as NIP-32 recommends for `l` namespaces:\n\n- `ext:org.edufeed.ekw:bistum:id`\n- `ext:org.edufeed.ekw.konfi:zielgruppen:id`\n\nA sub-vocabulary is a namespace of its own (`org.edufeed.ekw.konfi`), never a colon inside `<facet>`. Short unqualified slugs (`ekw`, `oersi`) remain valid and are common in existing data, but new authorities SHOULD prefer reverse-DNS.\n\n##### Scalar ext properties\n\nAn ext key with no `<sub>` carries a plain literal value:\n\n- `[\"ext:ekw:bibleReference\", \"Mt 5,1-12\"]`\n- `[\"ext:ekw:methodOther\", \"Bibliolog\"]`\n\nRepeated keys form an array of strings. On reverse conversion these surface as `output.ext.<ns>.<facet>` holding an array of strings, alongside concept facets, which hold an array of objects. Consumers MUST support both forms and MUST NOT discard a key merely because it lacks a `<sub>`.\n\n##### Mixed facets\n\nA `<ns>:<facet>` pair MAY carry concept tags and scalar tags at the same time. This is the ordinary shape for a field that offers a controlled vocabulary *and* accepts a free-text value, where an author uses both:\n\n- `[\"ext:org.edufeed.ekw.konfi:zeitstruktur:id\", \"https://w3id.org/kim/ekw/zeit/doppelstunde\"]`\n- `[\"ext:org.edufeed.ekw.konfi:zeitstruktur:prefLabel:de\", \"Doppelstunde\"]`\n- `[\"ext:org.edufeed.ekw.konfi:zeitstruktur:type\", \"Concept\"]`\n- `[\"ext:org.edufeed.ekw.konfi:zeitstruktur\", \"2 x 90 Min.\"]`\n\n`output.ext.<ns>.<facet>` is therefore a **heterogeneous array** — objects and strings may both appear in it:\n\n```json\n{\n  \"zeitstruktur\": [\n    { \"id\": \"https://w3id.org/kim/ekw/zeit/doppelstunde\", \"type\": \"Concept\", \"prefLabel\": { \"de\": \"Doppelstunde\" } },\n    \"2 x 90 Min.\"\n  ]\n}\n```\n\nConsumers MUST accumulate the two kinds independently. A consumer MUST NOT let the first tag it sees for a pair fix the kind of the whole facet: that discards every tag of the other kind, and since producers emit concept tags before scalars, the half discarded is the author's own text — invisibly, because the vocabulary half still renders.\n\nOrdering is normative, so that the reconstructed value re-serializes to the same tag set: **concepts first in tag order, then scalars in tag order**, irrespective of how the tags are interleaved in the event. `<sub>` tags continue to attach to the concept opened by the preceding `id`; a scalar tag sitting between them MUST NOT act as that boundary.\n\n##### Parsing rule (normative)\n\nConsumers MUST parse ext keys **left-anchored** on `:`, with fixed arity:\n\n```\next-key = \"ext\" \":\" ns \":\" facet [ \":\" sub ]\nsub     = \"id\" / \"type\" / \"name\" / \"prefLabel\" \":\" lang\n```\n\n1. Split the key on `:`. The first segment MUST be `ext`.\n2. The second segment is `<ns>`; the third is `<facet>`. Both MUST be non-empty.\n3. Everything after the third segment, rejoined with `:`, is `<sub>`. If absent, the tag is a scalar.\n4. If `<sub>` is present it MUST match the closed set above. `prefLabel` MUST be followed by exactly one language segment.\n5. A key that does not match this grammar MUST be ignored — consumers MUST NOT guess a segmentation, and MUST NOT absorb surplus segments into `<ns>`, `<facet>` or `<sub>`. Implementations SHOULD emit a warning so malformed producers are discoverable.\n\nRule 5 is load-bearing. Right-anchored heuristics (\"the last segment is the sub, everything before it is the namespace\") appear reasonable but assign different `(ns, facet)` pairs than left-anchored parsing whenever a key carries surplus segments, so two conformant-looking implementations can derive different metadata from identical bytes.\n\nProducers MUST NOT emit keys outside this grammar. In particular, `<ns>` and `<facet>` MUST be checked for `:` before serialization.\n\n##### Form-emitted ext (Edufeed convention)\n\nWhen a kind 30168 form produces ext fields, `<ns>` is derived from the form's `d`-tag (a colon-free slug per Edufeed convention), optionally reverse-DNS qualified — e.g. a form with `d`-tag `amb-basic` emits `ext:amb-basic:<fieldId>:id` or `ext:org.edufeed.forms.amb-basic:<fieldId>:id`. The form-author's pubkey is **not** in `<ns>` — it's discoverable via the resource's `[\"a\", \"30168:<pub>:<d>\", \"<relay>\", \"form\"]` back-ref. If two authors choose the same `<ns>`, the back-ref disambiguates which form was used; clients can layer `#a 30168:<pub>:<d>` to narrow.\n\n##### Migrating legacy shapes\n\nTwo non-conformant shapes exist in deployed data and both SHOULD be migrated.\n\n**Unprefixed namespaces.** Some events use a de-facto `<ns>:<facet>:<sub>` shape without the `ext:` prefix (notably from `amb-nostr-converter` and EKW pipelines). Producers SHOULD migrate to the prefixed form. Consumers MAY accept the unprefixed form for backward compatibility during a transition period, but the `ext:` prefix is the only forward-compatible shape because AMB-core may introduce new top-level properties that would otherwise collide.\n\n**Surplus segments.** Keys carrying more than one namespace or facet segment — `ext:<ns>:<sub-vocabulary>:<facet>:<sub>` or `ext:30168:<pubkey>:<d-tag>:<facet>:<sub>` — predate the parsing rule above and are ambiguous by construction. Producers MUST migrate them by promoting the surplus segment into `<ns>`:\n\n| Legacy | Conformant |\n| --- | --- |\n| `ext:ekw:konfi:zielgruppen:id` | `ext:org.edufeed.ekw.konfi:zielgruppen:id` |\n| `ext:30168:<pub>:amb-basic:fach:id` | `ext:amb-basic:fach:id` (pubkey moves to the `a` back-ref) |\n\nBecause the two segmentations are indistinguishable to a consumer, there is no safe backward-compatibility shim: per rule 5, consumers MUST ignore these keys rather than guess. Migration is a re-publish of the affected events.\n\n## How to convert an AMB nostr-event to AMB metadata\n\nTo convert a Nostr event back to AMB metadata:\n\n1. **Extract tags**: Get the `tags` array from the Nostr event\n2. **Group by prefix**: Collect all tags that share the same prefix (before the first `:`)\n3. **Reconstruct nesting**: Use the `:` delimiter to rebuild nested object structure\n4. **Handle arrays**: Multiple tags with identical keys become array elements\n5. **Preserve order**: Array order is determined by tag order in the event\n6. **Special mappings**:\n   - `d` tag → `id` property: if the `d` value is an absolute URI, use it verbatim; otherwise derive the `id` as `nostr:<naddr1...>` (the [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) `naddr` encoding of kind `30142`, the event's `pubkey`, and the `d` value). Consumers MAY substitute a dereferenceable landing-page URL they control for the derived `nostr:` URI.\n   - `content` field → `description` property (prefer over `description` tag if both exist)\n   - `t` tags → `keywords` array\n   - `r` tags → Nostr-native supplementary references (no AMB equivalent; not included in AMB output)\n   - `p` tags with role → Nostr-native creator/contributor (see below)\n   - `a` tags with role → Nostr-native relation (see below)\n   - Convert string booleans to actual booleans (e.g. `isAccessibleForFree`)\n   - Convert numeric strings back to integers where the AMB schema requires numbers (`suggestedAge:minValue`/`suggestedAge:maxValue`)\n   - Parse ISO8601 dates if needed for validation\n7. **Add `@context`**: The output MUST include `\"@context\": [\"https://w3id.org/kim/amb/context.jsonld\", {\"@language\": \"<lang>\"}]` — the AMB schema requires `@context` at the top level. The language is implementation-configurable (default: `de`).\n\n   > **Known limitation:** Custom or extended `@context` entries from a source AMB document (e.g. an additional `\"https://schema.org\"` entry) are not stored in the event and therefore cannot be restored on reverse conversion — the canonical two-element context is always reconstructed. Documents using only the standard AMB context round-trip losslessly.\n8. **Nostr-native `p` tags** (creator/contributor): For each `[\"p\", <pubkey-hex>, <relay-hint>, <role>]` where `<role>` is `\"creator\"` or `\"contributor\"`, clients SHOULD fetch the user's kind:0 profile (using the relay hint and NIP-65) to resolve their `name`. Map to an AMB creator/contributor object:\n   ```json\n   {\n     \"name\": \"<name from kind:0 profile>\",\n     \"type\": \"Person\",\n     \"id\": \"nostr:<nprofile1...>\"\n   }\n   ```\n   The `id` uses the NIP-19 `nprofile` encoding (which includes the pubkey and relay hint(s)) prefixed with `nostr:` per NIP-21. The `type` (`\"Person\"` or `\"Organization\"`) should be determined from the kind:0 profile if possible; implementations MAY default to `\"Person\"` when unknown.\n\n   If the kind:0 profile cannot be fetched (or the converter operates offline), `name` MUST fall back to the NIP-19 `npub` encoding of the pubkey — the AMB schema requires `name` and `type` on every creator/contributor object, so output must never omit them. Profile-aware clients SHOULD replace the fallback with the resolved profile name once available.\n9. **Nostr-native `a` tags** (relations): For each `[\"a\", \"30142:<pubkey>:<d-value>\", <relay-hint>, <role>]` where `<role>` is `\"isBasedOn\"`, `\"isPartOf\"`, or `\"hasPart\"`, map to the corresponding AMB relation object:\n   ```json\n   {\n     \"id\": \"nostr:<naddr1...>\",\n     \"type\": \"LearningResource\"\n   }\n   ```\n   The `id` uses the NIP-19 `naddr` encoding (which includes kind, pubkey, d-tag, and relay hint(s)) prefixed with `nostr:` per NIP-21.\n10. **Extension tags (`ext:` prefix)**: Parse each key whose first segment is `ext` using the normative left-anchored rule in *Extension Properties*, ignoring any key that does not match. Group the surviving tags by `(<namespace>, <facet>)`. Within each pair, apply the same flattening rules as AMB-core (boundary on repeated `id`, `prefLabel:<lang>` → `prefLabel.<lang>`); keys with no `<sub>` yield strings instead. Concepts and scalars under one pair accumulate independently and concatenate as concepts-then-scalars — see *Mixed facets*. Place the result under `output.ext.<namespace>.<facet>`. Implementations MUST NOT merge ext entries into AMB-core properties.\n\n\n## How to query for AMB nostr-events in supporting relays\n\nAMB-supporting relays MUST support the standard NIP-01 filter fields and SHOULD support NIP-50 full-text search with field-specific filtering.\n\n### Standard Nostr Filters (NIP-01)\n\nClients can query AMB events using standard Nostr filter fields:\n\n- `kinds` — filter by event kind (always `30142` for AMB events)\n- `authors` — filter by pubkey\n- `ids` — filter by event ID\n- `#d` — filter by the addressable event identifier (d-tag)\n- `since` / `until` — filter by `created_at` timestamp range\n\n### AMB Tag Filters\n\nIn addition to standard single-letter tag filters, AMB-supporting relays SHOULD support filtering by the colon-delimited tag names used in AMB events. The tag name in the filter maps directly to the flattened tag key in the event:\n\n| Tag Filter | Description |\n|---|---|\n| `#t` | Filter by keyword |\n| `#r` | Filter by external reference URL |\n| `#p` | Filter by creator/contributor pubkey |\n| `#a` | Filter by addressable event reference |\n| `#about:id` | Filter by subject (controlled vocabulary URI) |\n| `#learningResourceType:id` | Filter by resource type URI |\n| `#educationalLevel:id` | Filter by educational level URI |\n| `#audience:id` | Filter by target audience URI |\n| `#ext:<ns>:<facet>:id` | Filter by extension property URI within a namespace |\n| `#ext:<ns>:<facet>:prefLabel:<lang>` | Filter by extension property label |\n| `#ext:<ns>:<facet>` | Filter by scalar extension property value |\n\nAny colon-delimited tag name present in AMB events can be used as a filter. Multiple values for the same tag are matched with OR logic. Different tag filters are combined with AND logic.\n\n### NIP-50 Full-Text Search\n\nAMB-supporting relays SHOULD implement [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md) to allow full-text search across AMB metadata fields (at minimum: `name`, `description`, `keywords`).\n\nRelays MAY additionally support field-specific search filtering using dot-notation within the `search` string. The dot-notation maps to the nested AMB field structure (e.g., `publisher.name` maps to the `name` subfield of `publisher` objects):\n\n| Field Path | Description |\n|---|---|\n| `publisher.name` | Publisher organization name |\n| `creator.name` | Content creator name |\n| `about.prefLabel.<lang>` | Subject/topic label (e.g., `about.prefLabel.de`) |\n| `learningResourceType.prefLabel.<lang>` | Resource type label |\n| `audience.prefLabel.<lang>` | Target audience label |\n| `educationalLevel.prefLabel.<lang>` | Educational level label |\n| `ext.<ns>.<facet>.id` | Extension property URI |\n| `ext.<ns>.<facet>.prefLabel.<lang>` | Extension property label |\n| `ext.<ns>.<facet>.type` | Extension property RDF type |\n\nFree-text terms and field filters can be mixed in the search string. Multiple values for the same base field are combined with OR logic.\n\n### Query Examples\n\n#### JSON Filter Objects\n\n```json\n// All AMB events\n{\"kinds\": [30142]}\n\n// Events by a specific author\n{\"kinds\": [30142], \"authors\": [\"<pubkey-hex>\"]}\n\n// Lookup by addressable event coordinate (kind + pubkey + d-tag)\n{\"kinds\": [30142], \"authors\": [\"<pubkey-hex>\"], \"#d\": [\"<d-tag-value>\"]}\n\n// Events created in a time range\n{\"kinds\": [30142], \"since\": 1700000000, \"until\": 1800000000}\n\n// Filter by keyword\n{\"kinds\": [30142], \"#t\": [\"Mathematik\"]}\n\n// Filter by subject URI\n{\"kinds\": [30142], \"#about:id\": [\"http://w3id.org/kim/schulfaecher/s1017\"]}\n\n// Filter by learning resource type URI\n{\"kinds\": [30142], \"#learningResourceType:id\": [\"http://w3id.org/openeduhub/vocabs/new_lrt/video\"]}\n\n// Filter by educational level URI\n{\"kinds\": [30142], \"#educationalLevel:id\": [\"https://w3id.org/kim/educationalLevel/level_06\"]}\n\n// Filter by external reference\n{\"kinds\": [30142], \"#r\": [\"https://doi.org/10.1234/example\"]}\n\n// Filter by creator/contributor pubkey\n{\"kinds\": [30142], \"#p\": [\"<pubkey-hex>\"]}\n\n// Filter by addressable event reference\n{\"kinds\": [30142], \"#a\": [\"30142:<pubkey-hex>:<d-tag-value>\"]}\n\n// NIP-50 full-text search\n{\"kinds\": [30142], \"search\": \"pythagorean theorem\"}\n\n// NIP-50 search with field-specific filter\n{\"kinds\": [30142], \"search\": \"publisher.name:e-teaching.org\"}\n\n// NIP-50 combined: free text + field filter\n{\"kinds\": [30142], \"search\": \"forschung publisher.name:e-teaching.org\"}\n\n// NIP-50 multiple values for same field (OR logic)\n{\"kinds\": [30142], \"search\": \"about.prefLabel.de:Mathematik about.prefLabel.de:Physik\"}\n```\n\n#### nak CLI Examples\n\n```bash\n# All AMB events\nnak req -k 30142 ws://relay.example.com\n\n# By author\nnak req -a <pubkey-hex> -k 30142 ws://relay.example.com\n\n# By d-tag\nnak req -d \"https://oersi.org/resources/example123\" -k 30142 ws://relay.example.com\n\n# Time range\nnak req --since 1700000000 --until 1800000000 -k 30142 ws://relay.example.com\n\n# By keyword\nnak req -t t=Mathematik -k 30142 ws://relay.example.com\n\n# By subject URI\nnak req -t about:id=http://w3id.org/kim/schulfaecher/s1017 -k 30142 ws://relay.example.com\n\n# By learning resource type\nnak req -t learningResourceType:id=http://w3id.org/openeduhub/vocabs/new_lrt/video -k 30142 ws://relay.example.com\n\n# By external reference\nnak req -t r=https://doi.org/10.1234/example -k 30142 ws://relay.example.com\n\n# By creator/contributor pubkey\nnak req -p <pubkey-hex> -k 30142 ws://relay.example.com\n\n# Full-text search\nnak req --search \"pythagorean theorem\" -k 30142 ws://relay.example.com\n\n# Field-specific search\nnak req --search \"publisher.name:e-teaching.org\" -k 30142 ws://relay.example.com\n\n# Combined: free text + field filter\nnak req --search \"forschung publisher.name:e-teaching.org\" -k 30142 ws://relay.example.com\n```\n\n> **Note:** Relays that require [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) authentication need `--sec <key> --auth` flags with `nak`.\n\n### Reference Implementations\n\n- **[amb-relay](https://git.edufeed.org/edufeed/amb-relay)** — Nostr relay specialized for AMB events, built on the khatru relay framework\n- **[nostrlib/eventstore/typesense30142](https://git.edufeed.org/edufeed/nostrlib/src/branch/master/eventstore/typesense30142)** — Typesense-backed eventstore for kind 30142 events with full query documentation in its [README](https://git.edufeed.org/edufeed/nostrlib/src/branch/master/eventstore/typesense30142/README.md)\n\n## Examples\n\n### Example 1: Simple Educational Resource\n\n```json\n{\n  \"kind\": 30142,\n  \"id\": \"6ba638a3786cfce89af1702a36c59e0bd9206863afa5cb6b1299aaf0d9f48c84\",\n  \"pubkey\": \"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\n  \"created_at\": 1743419457,\n  \"tags\": [\n    [\"d\", \"https://oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM=11\"],\n    [\"type\", \"LearningResource\"],\n    [\"name\", \"Pythagorean Theorem Video\"],\n    [\"description\", \"An introductory video explaining the Pythagorean theorem\"],\n    [\"about:id\", \"http://w3id.org/kim/schulfaecher/s1017\"],\n    [\"about:prefLabel:de\", \"Mathematik\"],\n    [\"about:type\", \"Concept\"],\n    [\"about:id\", \"http://w3id.org/kim/schulfaecher/s1005\"],\n    [\"about:prefLabel:de\", \"Deutsch\"],\n    [\"about:type\", \"Concept\"],\n    [\"learningResourceType:id\", \"http://w3id.org/openeduhub/vocabs/new_lrt/7a6e9608-2554-4981-95dc-47ab9ba924de\"],\n    [\"learningResourceType:prefLabel:de\", \"Video\"],\n    [\"learningResourceType:type\", \"Concept\"],\n    [\"t\", \"Pythagoras\"],\n    [\"t\", \"Geometrie\"],\n    [\"t\", \"Mathematik\"],\n    [\"inLanguage\", \"de\"],\n    [\"license:id\", \"https://creativecommons.org/licenses/by/4.0/\"],\n    [\"isAccessibleForFree\", \"true\"]\n  ],\n  \"content\": \"An introductory video explaining the Pythagorean theorem\",\n  \"sig\": \"6b0b78d56dea322864d35ea3b6d7e892d0e62bed96cd11ecb27d6c1d0b6d0cd68cd9ec82419946a5fb3c8d4a21eca88c9a5dad47a3b3e466ba18787224a613ef\"\n}\n```\n\n### Example 2: Resource with Nostr-Native and External Creators\n\nThis example demonstrates both creator types: a Nostr-native creator (using a `p` tag only) and an external creator without a Nostr identity (using `creator:*` flattened tags).\n\n```json\n{\n  \"kind\": 30142,\n  \"id\": \"7ca749b4897efdc98fe2803dc60f68c9e1cd29764e8a55d1e9ef47a46ba4fe75\",\n  \"pubkey\": \"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\n  \"created_at\": 1743419500,\n  \"tags\": [\n    [\"d\", \"https://example.org/courses/physics-101\"],\n    [\"type\", \"LearningResource\"],\n    [\"type\", \"Course\"],\n    [\"name\", \"Introduction to Physics\"],\n    [\"description\", \"A comprehensive introduction to classical mechanics\"],\n    [\"p\", \"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\", \"wss://relay.example.com\", \"creator\"],\n    [\"creator:id\", \"https://orcid.org/0000-0009-8765-4321\"],\n    [\"creator:name\", \"Prof. John Doe\"],\n    [\"creator:type\", \"Person\"],\n    [\"creator:honorificPrefix\", \"Prof.\"],\n    [\"creator:affiliation:name\", \"Stanford University\"],\n    [\"creator:affiliation:type\", \"Organization\"],\n    [\"dateCreated\", \"2024-01-15\"],\n    [\"datePublished\", \"2024-02-01\"],\n    [\"about:id\", \"https://w3id.org/kim/hochschulfaechersystematik/n079\"],\n    [\"about:prefLabel:de\", \"Informatik\"],\n    [\"about:type\", \"Concept\"],\n    [\"learningResourceType:id\", \"https://w3id.org/kim/hcrt/course\"],\n    [\"learningResourceType:prefLabel:de\", \"Kurs\"],\n    [\"audience:id\", \"http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student\"],\n    [\"audience:prefLabel:de\", \"Student\"],\n    [\"audience:type\", \"Concept\"],\n    [\"educationalLevel:id\", \"https://w3id.org/kim/educationalLevel/level_06\"],\n    [\"educationalLevel:prefLabel:en\", \"Bachelor or equivalent\"],\n    [\"inLanguage\", \"en\"],\n    [\"license:id\", \"https://creativecommons.org/licenses/by-sa/4.0/\"],\n    [\"isAccessibleForFree\", \"true\"],\n    [\"r\", \"https://example.org/courses/physics-101\"],\n    [\"r\", \"https://doi.org/10.1234/physics-intro\"]\n  ],\n  \"content\": \"A comprehensive introduction to classical mechanics\",\n  \"sig\": \"8d1c89f5da33ec9a2b456def78a90b1cd23e456f78a90b12cd34e567f89a012b34c56d78e9f0a12bc3d45e6f78901a23b45c67d89e0f1a2b3c4d5e6f7890123a\"\n}\n```\n\nIn this example:\n- The first creator has a Nostr pubkey, so only a `p` tag with role `\"creator\"` is used. Their name and metadata are resolved from their kind:0 profile.\n- The second creator (Prof. John Doe) has no Nostr identity, so the `creator:*` flattened tags provide their name, type, affiliation, and ORCID.\n\n### Example 3: Resource with Extension Namespace\n\nThis example demonstrates the `ext:` namespace, used here to attach an EKW-specific `bistum` (diocese) facet that is not part of AMB-core. The same flattening grammar applies; the only difference is the leading `ext:<ns>:` prefix.\n\n```json\n{\n  \"kind\": 30142,\n  \"id\": \"9f4c2a1b8e7d3a6f5c2b9d8e7a1c4f3b8e2d9a7c5f1b3e8d6a4c2f9b7e5d3a1c\",\n  \"pubkey\": \"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\n  \"created_at\": 1764000000,\n  \"tags\": [\n    [\"d\", \"https://ekw.de/resources/abc\"],\n    [\"type\", \"LearningResource\"],\n    [\"name\", \"Religiöse Bildung im Bistum Hannover\"],\n    [\"about:id\", \"https://w3id.org/kim/hochschulfaechersystematik/n270\"],\n    [\"about:prefLabel:de\", \"Theologie\"],\n    [\"about:type\", \"Concept\"],\n    [\"ext:ekw:bistum:id\", \"https://w3id.org/kim/ekw/bistum/hannover\"],\n    [\"ext:ekw:bistum:prefLabel:de\", \"Hannover\"],\n    [\"ext:ekw:bistum:type\", \"Concept\"],\n    [\"ext:ekw:bistum:id\", \"https://w3id.org/kim/ekw/bistum/wuerttemberg\"],\n    [\"ext:ekw:bistum:prefLabel:de\", \"Württemberg\"],\n    [\"ext:ekw:bistum:type\", \"Concept\"]\n  ],\n  \"content\": \"\",\n  \"sig\": \"...\"\n}\n```\n\nOn reverse conversion to AMB metadata, the `ext` block surfaces as a sibling object (it MUST NOT be folded into AMB-core):\n\n```json\n{\n  \"type\": [\"LearningResource\"],\n  \"name\": \"Religiöse Bildung im Bistum Hannover\",\n  \"about\": [\n    {\"id\": \"https://w3id.org/kim/hochschulfaechersystematik/n270\", \"prefLabel\": {\"de\": \"Theologie\"}, \"type\": \"Concept\"}\n  ],\n  \"ext\": {\n    \"ekw\": {\n      \"bistum\": [\n        {\"id\": \"https://w3id.org/kim/ekw/bistum/hannover\", \"prefLabel\": {\"de\": \"Hannover\"}, \"type\": \"Concept\"},\n        {\"id\": \"https://w3id.org/kim/ekw/bistum/wuerttemberg\", \"prefLabel\": {\"de\": \"Württemberg\"}, \"type\": \"Concept\"}\n      ]\n    }\n  }\n}\n```\n\nConsumers that don't recognize the `ekw` namespace ignore it; consumers that do can render `bistum` generically (one row per concept, label resolved by language).\n\n## Tools\n\n### Using `nak` to create AMB events\n\nYou can use [`nak`](https://github.com/fiatjaf/nak) to create AMB events. There are two approaches:\n\n#### Flag-based (inline tags)\n\n```bash\n# Simple resource with Nostr-native t tags\nnak event \\\n  -k 30142 \\\n  --tag d=\"https://oersi.org/resources/example123\" \\\n  --tag type=\"LearningResource\" \\\n  --tag name=\"Pythagorean Theorem Video\" \\\n  --tag description=\"An introductory video\" \\\n  --tag about:id=\"http://w3id.org/kim/schulfaecher/s1017\" \\\n  --tag about:prefLabel:de=\"Mathematik\" \\\n  --tag t=\"Pythagoras\" \\\n  --tag t=\"Geometrie\" \\\n  --tag inLanguage=\"de\" \\\n  --tag license:id=\"https://creativecommons.org/licenses/by/4.0/\" \\\n  --sec <key> --auth ws://relay.example.com\n\n# Resource with Nostr-native creator (p tag)\nnak event \\\n  -k 30142 \\\n  --tag d=\"https://example.org/resource/456\" \\\n  --tag name=\"Physics Course\" \\\n  -p \"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798;wss://relay.example.com;creator\" \\\n  --sec <key> --auth ws://relay.example.com\n```\n\n#### JSON on stdin (pipe-based)\n\nThis approach gives full control over the tag structure and is useful for scripting:\n\n```bash\necho '{\n  \"tags\": [\n    [\"d\", \"https://example.org/courses/physics-101\"],\n    [\"type\", \"LearningResource\"],\n    [\"name\", \"Introduction to Physics\"],\n    [\"description\", \"A comprehensive introduction to classical mechanics\"],\n    [\"inLanguage\", \"en\"],\n    [\"t\", \"physics\"],\n    [\"t\", \"mechanics\"],\n    [\"creator:name\", \"Dr. Jane Smith\"],\n    [\"creator:type\", \"Person\"],\n    [\"license:id\", \"https://creativecommons.org/licenses/by-sa/4.0/\"]\n  ],\n  \"content\": \"A comprehensive introduction to classical mechanics\"\n}' | nak event -k 30142 --sec <key> --auth ws://relay.example.com\n```\n\n## References\n\n- [AMB Specification](https://dini-ag-kim.github.io/amb/latest/)\n- [Nostr Protocol (NIP-01)](https://github.com/nostr-protocol/nips/blob/master/01.md) - including addressable events (formerly NIP-33, merged into NIP-01)\n- [bech32-encoded entities (NIP-19)](https://github.com/nostr-protocol/nips/blob/master/19.md) - `nprofile` and `naddr` encodings for reverse conversion\n- [`nostr:` URI scheme (NIP-21)](https://github.com/nostr-protocol/nips/blob/master/21.md) - `nostr:` prefix for bech32 identifiers in AMB output\n- [Extra Metadata Fields and Tags (NIP-24)](https://github.com/nostr-protocol/nips/blob/master/24.md) - `r` and `t` tag conventions\n- [Live Activities (NIP-53)](https://github.com/nostr-protocol/nips/blob/master/53.md) - precedent for `p` tag roles\n- [Relay List Metadata (NIP-65)](https://github.com/nostr-protocol/nips/blob/master/65.md) - relay discovery for `p` tag relay hints\n- [JSON-Flattening Concept](https://localizely.com/json-flattener/)\n","sig":"841021e763c073f1d8bde5145685478a30d5dbc2d325540216cfe2521dcd56ad795658943e1da64a568e7a22d103cfcd1a3ebce99d50972a89a1405609ac78c9"}