StoneAI

Your first signed decision
in about fifteen minutes

Signup → decree → signed approval → receipt → your own verification · plain curl + Node

Six steps against the live /v1 API. No SDK, nothing to install beyond curl and Node 18+. Every /v1 response is enveloped: {"success":true,"data":{…}} on success, {"success":false,"error":…,"message":…} on failure.

01

Create your tenant

~2 min

One call creates your tenant on the Trial plan, its first API key, and its Ed25519 signing key pair. The response data holds tenantId, apiKey, signingPubKey, and verifyEmailSent. Passwords need at least 10 characters.

curl -s -X POST 'https://www.writteninstone.io/v1/signup' \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@company.com","password":"a-long-passphrase","company":"Acme"}'

Example data — for illustration.

Save two values now. The apiKey (sk_live_…) is shown exactly once; only its hash is stored. Pin the signingPubKey — step 5 verifies against the copy you keep, not the one we send later.
export STONE_KEY=sk_live_… · export SIGNING_PUBKEY=…

Signup is rate-limited to 10 requests per minute by default. You can re-read the public key any time with GET /v1/tenants/pubkey.

02

Raise your first decree

~3 min

Send a truth: any JSON facts about a decision in front of your systems, plus a domain. The model council deliberates and StoneAI returns a decree with status pending. StoneAI records the decision; it does not act in your systems.

curl -s -X POST 'https://www.writteninstone.io/v1/decrees' \
  -H "Authorization: Bearer $STONE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"domain":"ops","truth":{"signal":"502 spike","target":"checkout-service"}}'

Example data — for illustration.

From data, keep id (export DECREE_ID=…) and required_scope. You will also see action, confidence, and content_hash — the SHA-256 seal your approval will be bound to. The creation is written to your tenant's hash-chained audit ledger.

  • The domain sets the scope: opsPRODUCTION, moneyPAYMENT, complianceCOMPLIANCE, anything else → GENERAL.
  • If no council seat answers, the decree comes back with action hold.
  • The Trial plan includes 50 decrees a month; past that the call returns 402 quota_exceeded.
03

Approve it with the required scope

~2 min

Approve (or /deny) by presenting the decree's required_scope. StoneAI signs the decision with your tenant's Ed25519 key. The signature covers the tenant, decree, approver, scope, content hash, a random nonce, an expiry, and the verdict itself.

curl -s -X POST "https://www.writteninstone.io/v1/decrees/$DECREE_ID/approve" \
  -H "Authorization: Bearer $STONE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"approverId":"you@company.com","scope":"PRODUCTION"}'

Example data — for illustration.

Success returns {"status":"approved","decreeId":…}. A scope that doesn't match returns 403. A decree that already has a verdict returns 409 already_decided — it cannot be decided twice.

Know what is signed. StoneAI holds your tenant's signing key and signs on the approver's behalf. approverId is recorded as you send it; it is not yet tied to the authenticated user.
04

Fetch the Covenant Receipt

~1 min

A decided decree yields a receipt: the signed approval, the decree's content hash, your public key, the matching audit-ledger entry, and the newest Merkle anchor if one exists. Undecided decrees return 404.

curl -s "https://www.writteninstone.io/v1/decrees/$DECREE_ID/receipt" \
  -H "Authorization: Bearer $STONE_KEY" \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.stringify(JSON.parse(s).data,null,2)))' \
  > receipt.json
05

Verify it on your own machine

~2 min

Save this as verify.mjs. It uses only Node's built-in crypto and makes no network call. It checks the receipt against the public key you pinned in step 1.

// verify.mjs — Node 18+, zero dependencies
import { readFileSync } from 'node:fs';
import { createHash, createPublicKey, verify } from 'node:crypto';

const [file, pinnedKey] = process.argv.slice(2);
const r = JSON.parse(readFileSync(file, 'utf8'));
const a = r.approval;
const sha256 = (s) => createHash('sha256').update(s).digest();
const verdict = r.decision === 'approved' ? 'approve' : 'deny';

// The exact tuple StoneAI signs (v2 carries the verdict inside the signature).
const tuple = r.v === 2
  ? [2, a.tenantId, a.decreeId, a.approverId, a.scope, a.contentHash, a.nonce, a.expiry, a.decision]
  : [a.tenantId, a.decreeId, a.approverId, a.scope, a.contentHash, a.nonce, a.expiry];

// Raw 32-byte Ed25519 key -> SPKI DER, so node:crypto can use it.
const key = createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(pinnedKey, 'hex')]),
  format: 'der', type: 'spki',
});

const checks = {
  keyMatchesPinned: r.pubKey === pinnedKey,
  signature: verify(null, sha256(JSON.stringify(tuple)), key, Buffer.from(a.signature, 'hex')),
  boundToDecree: a.decreeId === r.decreeId && a.contentHash === r.contentHash,
  decision: r.chain.eventType === 'decree.' + r.decision && (r.v !== 2 || a.decision === verdict),
  auditEntry: r.chain.entryHash === sha256(JSON.stringify({
    tenantId: r.tenantId, eventType: r.chain.eventType, payload: r.chain.payload, prev: r.chain.prevHash,
  })).toString('hex'),
};
console.log(checks);
process.exit(Object.values(checks).every(Boolean) ? 0 : 1);
node verify.mjs receipt.json "$SIGNING_PUBKEY"

Every check prints true and the script exits 0. Change the verdict, the scope, the approver, or the content hash inside receipt.json and it fails.

What this proves. The decision was signed by your tenant's key, bound to that exact decree content, and recorded as a matching entry in your hash-chained ledger. What it does not prove: that a particular person pressed approve — StoneAI holds the signing key (see step 3). Anyone holding both IDs can also open the no-login proof page at /proof/<tenantId>/<decreeId>, which runs the same checks server-side.
06

Receive decree events by webhook (optional)

~5 min

Register one HTTPS endpoint and a shared secret of at least 16 characters. The secret is stored encrypted and never returned.

curl -s -X PUT 'https://www.writteninstone.io/v1/webhook' \
  -H "Authorization: Bearer $STONE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your-app.example/hooks/stoneai","secret":"replace-with-a-long-random-secret"}'

curl -s -X POST 'https://www.writteninstone.io/v1/webhook/test' \
  -H "Authorization: Bearer $STONE_KEY"

Example data — for illustration.

  • Events: decree.created, decree.approved, decree.denied (all sent to the one endpoint), plus webhook.test from the test call.
  • Body: {"id","event","payload","ts"}. id stays the same across retries — use it to dedupe.
  • Signature: header x-stoneai-signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your secret. Reject stale timestamps.
  • Delivery: 8-second timeout, retried after 30 seconds and again after 5 minutes. Every attempt is listed at GET /v1/webhook/deliveries.
Decided. Signed. Verifiable.

Every endpoint and schema: API reference · live health: status