Developers
Writing into an ARK Early access
Your platform or your AI proposes records. The owner reviews them at login and decides what becomes part of the ARK.
The record envelopeEarly access
Every field of record.schema.json. Fields marked read only are assigned by ARKZ and ignored on a proposal.
| Name | Type | Required | Limits | Description |
|---|---|---|---|---|
arp | "0" | yes | Protocol version. | |
id | string | no | pattern ^rec_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | Record id, assigned by ARKZ. Absent on a proposal. (read only) |
ref | string | no | pattern ^[a-z0-9_]+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(:[a-z_]+)?$ | Record reference, assigned by ARKZ: <table>:<uuid>, or <table>:<uuid>:<kind> when one row gives several kinds. Either id or ref names a record. (read only) |
ark | string | yes | pattern ^ark:arkz:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | The ARK this record belongs to. |
facet | "identity" | "belief_value" | "knowledge" | "framework" | "position_decision" | "experience_story" | "preference_boundary" | "voice" | "likeness" | "behavior_execution" | "relationship" | "provenance_consent" | "disposition" | "humor" | "mannerism" | "body_of_work" | yes | A region of the person. | |
kind | "name" | "bio" | "expertise" | "mastery" | "role" | "belief" | "value" | "opinion" | "identity" | "knowledge" | "skill" | "document" | "passage" | "raw" | "process" | "pattern" | "definition" | "correction" | "decision" | "position" | "experience" | "story" | "journal" | "preference" | "boundary" | "utterance" | "marker" | "profile_version" | "audio_profile" | "likeness_asset" | "face_reference" | "voiceprint" | "motion_reference" | "laugh_sample" | "consent_scope" | "likeness_model" | "habit" | "task" | "project" | "trade" | "person" | "interaction" | "certificate" | "grant" | "attestation" | "baseline" | "state" | "expression" | "trigger" | "coping" | "profile" | "sarcasm_index" | "laugh" | "bit" | "never" | "verbal" | "physical" | "ritual" | "rhythm" | "appearance" | "work" | "collection" | "credit" | "style_profile" | yes | A record type inside its facet. The pairing of facet and kind is checked below. | |
authorship | "self_verbatim" | "self_paraphrase" | "self_structured" | "about_subject" | "derived" | yes | Who said it and how. Only self_verbatim can ever feed voice; derived never stands as canon alone. | |
trust_tier | "T0" | "T1" | "T2" | "T3" | "T4" | no | Assigned by ARKZ from the credential, the grant and the authorship. Never set by a client; a proposal that sends it has it ignored. (read only) | |
privacy | "vault" | "training" | "licensable" | no | Most restrictive first. A vault record is never served by the protocol. On a proposal the tighter of the grant default and this value wins. | |
subject | object | no | Who the record is about. | |
subject.ref | string | yes | pattern ^(self|entity|person:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$ | |
subject.name | string | no | ||
content | object | yes | Text for insight kinds; structured for decisions, habits, tasks, projects, people and trades. A proposal carries at least one; a derived or metadata-only record read back may carry neither. | |
content.text | string | no | ||
content.structured | object | no | ||
domains | string[] | no | ||
topics | string[] | no | ||
time | object | no | ||
time.captured_at | string | null | no | format date-time | |
time.valid_from | string | null | no | format date-time | |
time.valid_until | string | null | no | format date-time | |
provenance | object | yes | Where the record came from. | |
provenance.source_kind | string | null | no | Free text, lower snake case. | |
provenance.source_ref | string | null | no | Where it came from, for example a URL. | |
provenance.grant_id | string | null | no | The source grant that wrote it; null when an owner key wrote it. | |
provenance.client_id | string | null | no | The OAuth client, owner key id or source key id. | |
provenance.content_sha256 | string | null | yes | pattern ^[0-9a-f]{64}$ | Lower-case hex SHA-256 of the canonical content. Required on every proposal; ARKZ recomputes it and refuses a mismatch. |
provenance.signature | string | null | no | Optional detached signature by the source's registered key. | |
lineage | object | no | ||
lineage.supersedes | string | null | no | ||
lineage.superseded_by | string | null | no | ||
lineage.derived_from | string[] | no | ||
status | "proposed" | "accepted" | "rejected" | "expired" | "superseded" | "revoked" | "held" | no | Assigned by ARKZ. |
Facets and kindsEarly access
A facet is a region of the person; a kind is a record type inside it. Only the proposable kinds can be sent; the rest are made by ARKZ.
| Facet | Kinds | Proposable kinds |
|---|---|---|
identity | name, bio, expertise, mastery, role | none |
belief_value | belief, value, opinion, identity | belief, value, opinion, identity |
knowledge | knowledge, skill, document, passage, raw | knowledge, passage, raw |
framework | process, pattern, definition, correction | process, pattern, definition |
position_decision | decision, position | decision, position |
experience_story | experience, story, journal | experience, story, journal |
preference_boundary | preference, boundary | preference, boundary |
voice | utterance, marker, profile_version | utterance |
likeness | audio_profile, likeness_asset, face_reference, voiceprint, motion_reference, laugh_sample, consent_scope, likeness_model | none |
behavior_execution | habit, task, project, trade | habit, task, project, trade |
relationship | person, interaction | person, interaction |
provenance_consent | certificate, grant, attestation | none |
disposition | baseline, state, expression, trigger, coping | none |
humor | profile, sarcasm_index, laugh, bit, never | none |
mannerism | verbal, physical, ritual, rhythm, appearance | none |
body_of_work | work, collection, credit, style_profile | none |
Trust tiersEarly access
A trust tier is assigned by ARKZ from the credential, the grant and the authorship. A caller never sets it; a proposal that sends trust_tier has it ignored.
T0 T1 T2 T3 T4
PrivacyEarly access
Most restrictive first. A vault record is never served by the protocol. On a proposal, the tighter of the grant default and the value sent wins.
vault training licensable
The content hashEarly access
Every proposal carries provenance.content_sha256: lower-case hex SHA-256 of the canonical content, UTF-8 encoded. ARKZ recomputes it and refuses a mismatch. The canonical content is the text as given; when content.structured is a non-empty object it is the text, a newline, then the structured object in RFC 8785 canonical JSON (keys sorted, no spaces).
These snippets canonicalize ASCII keys, strings and integers correctly. Floats and non-ASCII keys need a real RFC 8785 library.
Hash fixture: text only (the hash is over the text as given)
{
"text": "Ship the smallest thing that proves the idea."
}Ship the smallest thing that proves the idea.14fdf7de13a45ffd735d60ef9a3aa2b4da5612a106e9b6f0faf9722f06aec75cconst { createHash } = require('node:crypto')
// RFC 8785 for ASCII keys, strings and integers: keys sorted, no spaces.
const jcs = (v) => Array.isArray(v) ? `[${v.map(jcs).join(',')}]`
: v !== null && typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${jcs(v[k])}`).join(',')}}`
: JSON.stringify(v)
const canonical = (c) => (c.text ?? '') +
(c.structured && Object.keys(c.structured).length ? '\n' + jcs(c.structured) : '')
const content = {"text":"Ship the smallest thing that proves the idea."}
console.log(canonical(content) === "Ship the smallest thing that proves the idea.")
console.log(createHash('sha256').update(canonical(content), 'utf8').digest('hex'))
// 14fdf7de13a45ffd735d60ef9a3aa2b4da5612a106e9b6f0faf9722f06aec75cimport hashlib, json
content = json.loads("{\"text\":\"Ship the smallest thing that proves the idea.\"}")
s = content.get('structured')
canon = content.get('text', '') + ('\n' + json.dumps(s, sort_keys=True, separators=(',',':'), ensure_ascii=False) if s else '')
print(canon == "Ship the smallest thing that proves the idea.")
print(hashlib.sha256(canon.encode('utf-8')).hexdigest())
# 14fdf7de13a45ffd735d60ef9a3aa2b4da5612a106e9b6f0faf9722f06aec75cprintf '%s' 'Ship the smallest thing that proves the idea.' | shasum -a 256
# 14fdf7de13a45ffd735d60ef9a3aa2b4da5612a106e9b6f0faf9722f06aec75c -Hash fixture: text plus structured (text, a newline, then the structured value as JCS)
{
"text": "Three steps to a first draft.",
"structured": {
"steps": 3,
"name": "first-draft",
"order": [
"outline",
"draft",
"cut"
]
}
}Three steps to a first draft.
{"name":"first-draft","order":["outline","draft","cut"],"steps":3}8c449eb93e00db519c830a86ecedd96384944a6bb1755426654c17eb59bb9f76const { createHash } = require('node:crypto')
// RFC 8785 for ASCII keys, strings and integers: keys sorted, no spaces.
const jcs = (v) => Array.isArray(v) ? `[${v.map(jcs).join(',')}]`
: v !== null && typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${jcs(v[k])}`).join(',')}}`
: JSON.stringify(v)
const canonical = (c) => (c.text ?? '') +
(c.structured && Object.keys(c.structured).length ? '\n' + jcs(c.structured) : '')
const content = {"text":"Three steps to a first draft.","structured":{"steps":3,"name":"first-draft","order":["outline","draft","cut"]}}
console.log(canonical(content) === "Three steps to a first draft.\n{\"name\":\"first-draft\",\"order\":[\"outline\",\"draft\",\"cut\"],\"steps\":3}")
console.log(createHash('sha256').update(canonical(content), 'utf8').digest('hex'))
// 8c449eb93e00db519c830a86ecedd96384944a6bb1755426654c17eb59bb9f76import hashlib, json
content = json.loads("{\"text\":\"Three steps to a first draft.\",\"structured\":{\"steps\":3,\"name\":\"first-draft\",\"order\":[\"outline\",\"draft\",\"cut\"]}}")
s = content.get('structured')
canon = content.get('text', '') + ('\n' + json.dumps(s, sort_keys=True, separators=(',',':'), ensure_ascii=False) if s else '')
print(canon == "Three steps to a first draft.\n{\"name\":\"first-draft\",\"order\":[\"outline\",\"draft\",\"cut\"],\"steps\":3}")
print(hashlib.sha256(canon.encode('utf-8')).hexdigest())
# 8c449eb93e00db519c830a86ecedd96384944a6bb1755426654c17eb59bb9f76printf '%s' 'Three steps to a first draft.
{"name":"first-draft","order":["outline","draft","cut"],"steps":3}' | shasum -a 256
# 8c449eb93e00db519c830a86ecedd96384944a6bb1755426654c17eb59bb9f76 -LanesEarly access
- Typed: a proposable facet and kind with its content. This is the normal lane.
- Raw: material such as a transcript, sent as knowledge/raw. It is held as the owner received it until the owner runs Extract on it in the proposals panel.
- Voice: a voice/utterance proposal is checked at intake (it must be self_verbatim) and stored, but acceptance always refuses it in v0. Do not send voice.
Batches, dedupe and idempotencyEarly access
propose_ark_records takes a batch and answers one result per envelope, in order; one refused item never stops the rest. A record your source already proposed answers status duplicate with duplicate_of naming the earlier proposal, so a retry after a timeout is safe: resend the same envelopes.
QuotasEarly access
Proposals count against quotas that are separate from call limits: a source grant has its own daily proposal quota, and an owner key or OAuth credential has its own. The values:
| Limit | Value | Unit | Defined in |
|---|---|---|---|
grant_proposal_quota | 200 | proposals per UTC calendar date per grant (a quota, not a call limit; the owner may set 1 to 5000) | supabase/migrations/20261215110000_arp_p1_keys_and_ledger.sql:41 |
credential_proposal_quota | 2,000 | proposals per UTC calendar date per owner key or OAuth connection without a grant (a quota, not a call limit) | supabase/migrations/20261216100000_arp_p2_proposals.sql:251 |
The subject and about_subjectEarly access
- subject.ref says who a record is about: self, an entity, or person:<uuid>.
- The subject-reserved rule: only an owner key may propose a boundary, an identity record or a relationship record. From an OAuth credential or a source key these answer subject_reserved.
- about_subject marks material written by someone other than the owner about the owner. It is kept as context for answers. It never becomes canon and never feeds voice. Whether a source grant may send it is set on the grant.
- The verbatim rule: self_verbatim means the exact words. Never invent, tidy or translate words and label them verbatim.
Worked examplesEarly access
A definition the owner wrote, in their own words
{
"arp": "0",
"ark": "ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d",
"facet": "framework",
"kind": "definition",
"authorship": "self_paraphrase",
"privacy": "training",
"content": {
"text": "A keystone habit is one routine that pulls the rest of a plan along with it. Name it, guard it, and let the other habits hang from it."
},
"topics": [
"habits",
"planning"
],
"provenance": {
"content_sha256": "767df8399817b451238ab1075a2bc55a8eb9cc144aaad4a969814e528308dd5a",
"source_kind": "notes_app"
}
}A story the owner told, retold faithfully
{
"arp": "0",
"ark": "ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d",
"facet": "experience_story",
"kind": "story",
"authorship": "self_paraphrase",
"content": {
"text": "I priced my first workshop by asking three past clients what it had changed for them. Their answers set the price, not my nerves."
},
"domains": [
"business"
],
"provenance": {
"content_sha256": "a4e3655a5c16aee2aa907c5cea50a559205255ed1c54a3428cc20a258b5eeed0",
"source_kind": "interview_notes"
}
}A raw transcript, word for word (the raw lane)
{
"arp": "0",
"ark": "ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d",
"facet": "knowledge",
"kind": "raw",
"authorship": "self_verbatim",
"content": {
"text": "Q: What do you check first in a new plan?\nA: Who owns each step. If nobody owns it, it is not a step yet."
},
"provenance": {
"content_sha256": "6f9e3498285055a9734c9634e4229be548989eb2e6877a7e41bda6f4c27ca69e",
"source_kind": "transcript",
"source_ref": "https://example.com/episodes/12"
}
}A testimonial someone else wrote about the owner (about_subject)
{
"arp": "0",
"ark": "ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d",
"facet": "experience_story",
"kind": "experience",
"authorship": "about_subject",
"subject": {
"ref": "self"
},
"content": {
"text": "Working with Dana, our team stopped arguing about priorities. She made us write the one decision we were avoiding at the top of every agenda."
},
"provenance": {
"content_sha256": "25e7a8d61eaa7b8f052d022f97c59fac982c515ce8a5560a64202111dfc2fdce",
"source_kind": "client_testimonial"
}
}curl -s https://www.arkz.ai/api/human \
-H "Authorization: Bearer $ARKZ_KEY" \
-H 'Content-Type: application/json' \
-d '{"action":"propose_ark_record","envelope":{"arp":"0","ark":"ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d","facet":"framework","kind":"definition","authorship":"self_paraphrase","privacy":"training","content":{"text":"A keystone habit is one routine that pulls the rest of a plan along with it. Name it, guard it, and let the other habits hang from it."},"topics":["habits","planning"],"provenance":{"content_sha256":"767df8399817b451238ab1075a2bc55a8eb9cc144aaad4a969814e528308dd5a","source_kind":"notes_app"}}}'curl -s https://www.arkz.ai/api/human \
-H "Authorization: Bearer $ARKZ_KEY" \
-H 'Content-Type: application/json' \
-d '{"action":"propose_ark_records","envelopes":[{"arp":"0","ark":"ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d","facet":"framework","kind":"definition","authorship":"self_paraphrase","privacy":"training","content":{"text":"A keystone habit is one routine that pulls the rest of a plan along with it. Name it, guard it, and let the other habits hang from it."},"topics":["habits","planning"],"provenance":{"content_sha256":"767df8399817b451238ab1075a2bc55a8eb9cc144aaad4a969814e528308dd5a","source_kind":"notes_app"}},{"arp":"0","ark":"ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d","facet":"experience_story","kind":"story","authorship":"self_paraphrase","content":{"text":"I priced my first workshop by asking three past clients what it had changed for them. Their answers set the price, not my nerves."},"domains":["business"],"provenance":{"content_sha256":"a4e3655a5c16aee2aa907c5cea50a559205255ed1c54a3428cc20a258b5eeed0","source_kind":"interview_notes"}},{"arp":"0","ark":"ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d","facet":"knowledge","kind":"raw","authorship":"self_verbatim","content":{"text":"Q: What do you check first in a new plan?\nA: Who owns each step. If nobody owns it, it is not a step yet."},"provenance":{"content_sha256":"6f9e3498285055a9734c9634e4229be548989eb2e6877a7e41bda6f4c27ca69e","source_kind":"transcript","source_ref":"https://example.com/episodes/12"}},{"arp":"0","ark":"ark:arkz:3f6c1a52-8d4e-4b7a-9c2f-5e1d0a7b8c9d","facet":"experience_story","kind":"experience","authorship":"about_subject","subject":{"ref":"self"},"content":{"text":"Working with Dana, our team stopped arguing about priorities. She made us write the one decision we were avoiding at the top of every agenda."},"provenance":{"content_sha256":"25e7a8d61eaa7b8f052d022f97c59fac982c515ce8a5560a64202111dfc2fdce","source_kind":"client_testimonial"}}]}'What happens nextEarly access
- The proposal waits. Nothing joins the ARK on its own.
- The owner reviews it at login in the proposals panel and accepts, rejects or holds it.
- Nothing is spent until the owner accepts.
- An accepted record counts as one ingested insight on the owner's plan, and the AI that files it is charged in actions at the owner's rate.
- A source never pays to write.
Checking what happenedEarly access
get_ark_proposal_status answers the status of proposals your credential wrote (a source key sees every proposal of its grant). Send proposal_ids to check specific ones, in order, or omit them to page through your proposals newest first with since, limit and after. It answers status only: no content, no owner note, no reason.
| Status | Meaning |
|---|---|
proposed | Waiting for the owner. decided_at is null. |
held | The owner set it aside to decide later. decided_at is null. |
accepted | Part of the ARK. An owner key or an OAuth credential that also holds read:records gets record_ref; a source key never does. |
rejected | The owner declined it. |
expired | It waited past its expiry without a decision. |
not_found | No such proposal, or not yours. The two answer the same. |
Rejected proposals are removed 30 days after the decision and expired ones 30 days after their scheduled expiry; after that their ids answer not_found. The job that expires and removes proposals is not yet scheduled: until it runs, an undecided proposal stays proposed and older ids keep answering.
const API = 'https://www.arkz.ai/api/human'
const KEY = process.env.ARKZ_KEY
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const call = (action, params) => fetch(API, {
method: 'POST',
headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ action, ...params }),
}).then((r) => (r.status === 429 || r.status >= 500 ? Promise.reject(new Error('retry ' + r.status)) : r.json()))
// Retry with backoff; safe for proposals because a resend answers duplicate.
const withRetry = (fn, wait = 2000, tries = 6) =>
fn().catch((e) => (tries <= 1 ? Promise.reject(e) : sleep(wait).then(() => withRetry(fn, wait * 2, tries - 1))))
// Ask about the open ids in batches of 100, backing off between rounds.
async function waitForDecisions(ids, rounds = 12) {
const decided = new Map()
let wait = 30_000
for (let round = 0; round < rounds && decided.size < ids.length; round++) {
const open = ids.filter((id) => !decided.has(id))
for (let i = 0; i < open.length; i += 100) {
const answer = await withRetry(() => call('get_ark_proposal_status', { proposal_ids: open.slice(i, i + 100) }))
for (const p of answer.proposals) if (p.status !== 'proposed' && p.status !== 'held') decided.set(p.proposal_id, p.status)
}
if (decided.size < ids.length) { await sleep(wait); wait = Math.min(wait * 2, 6 * 3600 * 1000) }
}
console.log([...decided].filter(([, s]) => s === 'accepted').length, 'accepted of', ids.length)
}
await waitForDecisions(['<proposal id>', '<proposal id>'])