← SoulInTime

For Builders

Hash in, proof file out

Live: createMoment, v1Verify, v1Proof, and getMoment to read one receipt. The firm key is not live. firmStamp is.

Anchor — not live. A signed-in question reads only that account's receipts, may quote one, and cannot say the words are true.

Firm key — not live. A key will POST up to 500 hashes, no bytes, and a webhook fires only when height is above 0.

The proof file is served at soulintime.com. The bytes are still in platform storage. Keep your copy.

Three calls. Checking is free. A height shows only when Bitcoin has the digest. No key. Spec: openapi.yaml · llms.txt

If you are an agent.

Three calls: createMoment, v1Verify, v1Proof, plus getMoment to read one receipt.

Trust a height only when status is confirmed.

a-hash-is-a-door-that-2026-019bd305@965906

Cite as slug@height only when status is confirmed.

The receipt file is the object to keep. The page is optional.

A receipt file, <slug>.receipt.json, holds slug, sha256, status, blockHeight (confirmed only), blockTime, statement, proofFile and proofBase64. A thread downloads as <label>.receipts.json, an array of those in height order.

/functions/llmsTxt · /functions/openapi

POST /functions/createMoment — hash in, proof file out
curl -X POST "https://soulintime.com/functions/createMoment" \
  -H "Content-Type: application/json" \
  -d '{"sha256Hash":"<64 hex chars>"}'
200 application/json
{"slug":"<slug>",
 "proofFile":"https://soulintime.com/functions/v1Proof?id=<slug>&ots=1",
 "status":"in_calendar"}

429 if the hash already has a receipt, or on a second Create from one address inside the hour:
{"error":"Already stamped or rate limited. Check the existing receipt.",
 "slug":"a-hash-is-a-door-that-2026-019bd305"}

400 if sha256Hash is missing:
{"error":"sha256Hash is required (64 hex characters)."}

Documented, not executed on this page. Send only a SHA-256; your bytes stay with you. The reply has only slug, proofFile, status and blockHeight. proofFile appears only when a proof file exists. blockHeight appears only as a whole number above 0. One fresh hash per hour from this address. Try again later. A hash that already has a receipt, or a second Create inside that hour, returns 429 with the existing slug when the receipt is not private. A missing sha256Hash returns 400.

GET /functions/v1Verify — status of exact bytes
curl "https://soulintime.com/functions/v1Verify?hash=019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f"
200 application/json
{"hash":"019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f",
 "status":"confirmed",
 "blockHeight":965906,
 "proofFile":"https://soulintime.com/functions/v1Proof?id=a-hash-is-a-door-that-2026-019bd305&ots=1",
 "checkYourself":["curl -L -o proof.ots …","pip install opentimestamps-client || pipx install opentimestamps-client","ots verify -d 019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f proof.ots"],
 "slug":"a-hash-is-a-door-that-2026-019bd305",
 "url":"https://soulintime.com/moment/a-hash-is-a-door-that-2026-019bd305"}

A different file is a different hash. blockHeight appears only for a real height; blockTime only when the block's UTC time is stored. A pending row has neither key. proofFile appears only when an https proof file exists and serves the exact bytes from soulintime.com; proof.mempoolUrl only for a real height. checkYourself is absent when there is no https .ots. Trust a height only when status is confirmed: that means a height plus a proof file you can check yourself. height_asserted is a height with no proof file.

Check a list of hashes
while read h; do r=$(curl -s "https://soulintime.com/functions/v1Verify?hash=$h"); printf '%s %s %s\n' "$h" "$(echo "$r" | grep -oE '"(status|error)":"[^"]*"')" "$(echo "$r" | grep -oE '"(blockHeight|heightAsserted)":[0-9]+')"; done < hashes.txt
019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f "status":"confirmed" "blockHeight":965906
897dc6feeee3e23f6f455e39499368e08b448abd08a825ce973ca420ac203364 "status":"height_asserted" "heightAsserted":964013
0000000000000000000000000000000000000000000000000000000000000000 "error":"not_found" 

One hash per line. confirmed with a height means those bytes existed by that block. 404 means no record here. height_asserted is not proof. Status comes first. Trust a height only when status is confirmed.

GET /functions/v1Proof — the proof for one receipt
curl "https://soulintime.com/functions/v1Proof?id=a-hash-is-a-door-that-2026-019bd305"
200 application/json
{"hash":"019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f",
 "status":"confirmed",
 "blockHeight":965906,
 "slug":"a-hash-is-a-door-that-2026-019bd305",
 "proofFile":"https://soulintime.com/functions/v1Proof?id=a-hash-is-a-door-that-2026-019bd305&ots=1",
 "proof":{"mempoolUrl":"https://mempool.space/block-height/965906",
          "verifyUrl":"https://soulintime.com/verify?hash=019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f"},
 "checkYourself":["curl -L -o proof.ots …","pip install opentimestamps-client || pipx install opentimestamps-client","ots verify -d 019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f proof.ots"]}

Check the .ots yourself with the OpenTimestamps client. You never have to trust this API.

For an agent

Hash on your side. Never send the file. A video is a hash.

A body with only sha256Hash still returns a slug. Build the proof URL from that slug.

POST https://soulintime.com/functions/createMoment — body {"sha256Hash":"<64 hex>"}. Hash on their side. Never send the file.

One fresh hash per hour from this address. Try again later. A hash that already has a receipt, or a second Create inside that hour, returns 429 {"error":"Already stamped or rate limited. Check the existing receipt."}, with the existing slug when the receipt is not private. A body with no 64-hex sha256Hash returns 400. Every createMoment and v1Verify reply carries X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds).

GET https://soulintime.com/functions/v1Verify?hash= — Trust blockHeight only when status is "confirmed". height_asserted is not proof.

A height is orange only when status is confirmed and a proof file exists. height_asserted is not proof. GET https://soulintime.com/functions/v1FirstAt?list=1 returns the public confirmed receipts, newest first.

The public list is newest first. It does not include the height-only row. GET https://soulintime.com/functions/v1FirstAt?list=1.

The clock is blockHeight when status is confirmed. The chain tip is not the clock.

GET https://soulintime.com/functions/v1Proof?id=<slug>&ots=1 — returns the proof bytes. Keep the file.

Keep the proof file. Our copy still sits in platform storage.

GET https://soulintime.com/functions/getMoment?slug=<slug> — read one receipt: slug, sha256Hash, status, blockHeight only above 0, heightAsserted (Height only, not proof), blockTime (confirmed only), proofFile, created_date, title, placeCaption, visibility (public or unlisted), thread (public receipts only), openAfter, mediaUrl. A key is absent when it has no value. A private receipt returns 403.

GET https://soulintime.com/functions/getMoment?thread=<label> — the public receipts with that thread label, lowest height first, then by date: slug, title, status, blockHeight (confirmed only), created_date. An empty label returns [].

Hash on your side. The file never arrives.

An optional thread label can be sent as thread. It is plain text. It is not proof.

A thread label on a public receipt is public. On an unlisted or private receipt it stays with the owner.

Composed receipt (schema v1)

A composed receipt is a hash of a list. The list is not uploaded. Check the object file yourself.

Several files and a line, dated as one receipt. Hash each part on your side, list those hashes in one small JSON object, and send the sha256 of the object. The parts and the object never reach us.

{"schema":"soulintime.moment","v":1,"parts":[{"sha256":"<64 hex>","mime":"image/jpeg","bytes":14},{"sha256":"<64 hex>","kind":"text"}],"meta":{"label":"garden"},"prior":["<slug>"],"visibility":"unlisted"}

parts keeps its order. Each part has sha256 (64 lowercase hex). mime, bytes (a whole number) and kind ("text" for a typed line) are optional. File names stay out. A line part is the sha256 of the trimmed UTF-8 line, the same hash Check the words gives on /verify.

meta holds up to 16 keys (a-z, 0-9, _, up to 32 characters), each a string up to 280 characters or a whole number. label is a short name. device_time is device time, not the clock; the Bitcoin height is the clock. place_sha256 is the sha256 of "<salt>|<lat>,<lng>" at 4 decimals, and the salt stays with the author.

prior lists up to 32 receipt slugs. A reference only; it says nothing about the other receipt. visibility is private, unlisted or public, as the author chose. Every field is optional.

canonical bytes

1. JSON, UTF-8, no BOM, no whitespace outside strings, no trailing newline.
2. Object keys sorted ascending at every depth.
3. Arrays keep their order.
4. Strings written the way JSON.stringify writes them.
5. Whole numbers only. No null: leave the key out.
6. Hex is lowercase.
7. Receipt hash = sha256 of those bytes, lowercase hex.

Python: json.dumps(obj, sort_keys=True, separators=(',',':'), ensure_ascii=False).encode('utf-8')

test vector

{"meta":{"device_time":"2026-10-11T10:45:00Z","label":"garden","place_sha256":"01eeb7f6c9c3a8f0f4c49f4cccec735c820dc4033a1d6ebb36adccb6e834c1d6"},"parts":[{"bytes":14,"mime":"image/jpeg","sha256":"526f767300233fa74c98e3588457643c593dd4cf26c31b2de01c1cac3b8a59a0"},{"bytes":14,"mime":"audio/mp4","sha256":"20934dceedc0ff28e52e275fdbe085246da2e9020a9c8ca97916a0b2bd7b953a"},{"bytes":16,"mime":"application/pdf","sha256":"c2ccb2df27a79fb97c54378d4411c444ef5454e3aa3c24d385acf6d913e42f98"},{"kind":"text","sha256":"ee3d14f6d7c655b474f349a634633e0be33f70ff7ae44d6b76ec9d55dd8fe22c"}],"prior":["moment-muq37ke0"],"schema":"soulintime.moment","v":1,"visibility":"unlisted"}

sha256: a079764977f1a18e65f56da475912cb9a0d565ce29b9783d1e0cb520cf11477e

POST https://soulintime.com/functions/createMoment

{"sha256Hash":"<sha256 of the canonical bytes>","blockType":"composed"}

blockType is optional; it lets the receipt page say "Composed receipt". The reply is the same four keys: slug, proofFile, status, blockHeight.

Keep the object file as <slug>.moment.json. Drop it on /verify to see its parts and the heights of the receipts it refers to. Drop a part, or type the line, to match it. No account.

The receipt proves those object bytes existed by the height. A part is in it only if its sha256 is listed. It never says the contents are true.

Status replies

pending

{"status":"in_calendar","proofFile":"https://soulintime.com/functions/v1Proof?id=<slug>&ots=1"}

no blockHeight key.

confirmed

{"status":"confirmed","blockHeight":965906,"proofFile":"https://soulintime.com/functions/v1Proof?id=a-hash-is-a-door-that-2026-019bd305&ots=1"}

height_asserted

{"status":"height_asserted","heightAsserted":964013}

no proofFile key.

Trust the height only when status is confirmed.

Hash in, row back. No key.

do not send the file, send the hash.

// Hash on your side. Send the hash, never the file.
const hash = "<64 hex>";
const made = await fetch("https://soulintime.com/functions/createMoment", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sha256Hash: hash }),
}).then(r => r.json());
const check = await fetch("https://soulintime.com/functions/v1Verify?hash=" + hash).then(r => r.json());
// Trust check.blockHeight only when check.status === "confirmed".
const ots = await fetch("https://soulintime.com/functions/v1Proof?id=" + made.slug + "&ots=1");
const proofFile = new Uint8Array(await ots.arrayBuffer());
// Keep proofFile with the original. ots verify works without us.

Agent files point to https://soulintime.com/contact and hello@soulintime.com.

The hash is the proof. A signed-in preview is not the proof. Agents send the hash, never the file.

Citing, packs, recovery

One stable entry point for agents: https://soulintime.com/functions/llmsTxt. /agents.json points to it.

Moment schema. soulintime.moment v1: {"schema":"soulintime.moment","v":1,"parts":[{"sha256"}],"meta":{},"prior":["<slug>"],"visibility"}, every field optional, canonical JSON (keys sorted, no whitespace, whole numbers, no null, UTF-8); its sha256 is the receipt hash. Details above.

Citing contract. Cite a receipt as slug@height only when GET https://soulintime.com/functions/v1Verify?hash={sha256} returns status "confirmed" with that blockHeight. Otherwise refuse to cite it and say the receipt is pending or missing. A status written in a pack is not enough; check v1Verify. The chain tip is never a height for a receipt.

Context pack. Context pack (schema soulintime.pack v1), JSON: {"schema":"soulintime.pack","v":1,"receipts":[{"slug","hash","proofFile","status","blockHeight"}],"excerpts":[{"slug","text"}]}. blockHeight appears only when status is confirmed. excerpts appear only if the holder chose to include them. An agent may quote only those excerpts and cite only the receipts listed, each under the citing rule.

context pack

{"schema":"soulintime.pack","v":1,"receipts":[{"slug":"a-hash-is-a-door-that-2026-019bd305","hash":"019bd3056ae35f6eee3f61fcabdaabcfe5f374c07168a7e4d6fa38a828cbac2f","proofFile":"https://soulintime.com/functions/v1Proof?id=a-hash-is-a-door-that-2026-019bd305&ots=1","status":"confirmed","blockHeight":965906}],"excerpts":[{"slug":"a-hash-is-a-door-that-2026-019bd305","text":"A hash is a door that only one set of bytes can open."}]}

Recovery. The object file is required to display the parts of a composed receipt; we never hold it. Each part is checked against its own receipt (look up the part's sha256 with v1Verify), never against the object's height. The object's height covers the object hash only.

Bookmarklet / extension

Highlight text on any page. The browser hashes it with SHA-256 (the selected text, trimmed, UTF-8) and sends only the hash: POST https://soulintime.com/functions/createMoment with {"sha256Hash":"<64 hex>"}. The reply's slug gives the receipt URL, https://soulintime.com/moment/<slug>. The text never leaves the page.

The guest rule applies: one fresh hash per hour from this address. A page that blocks outside requests stops it; use /try there.

javascript:(async()=>{const t=String(getSelection()).trim();if(!t){alert('Select text first.');return}const d=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(t));const h=[...new Uint8Array(d)].map(b=>b.toString(16).padStart(2,'0')).join('');const r=await fetch('https://soulintime.com/functions/createMoment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sha256Hash:h})});const j=await r.json().catch(()=>({}));if(j.slug)prompt('Receipt','https://soulintime.com/moment/'+j.slug);else alert(j.error||('Error '+r.status))})()

Save it as a bookmark's address. A spec and a bookmarklet, not a store extension.

MCP / skill

stamp(content | filePath, thread?) -> { slug, proofFile }

1. Hash locally: SHA-256 of the content's UTF-8 bytes, or of the file's bytes.
2. POST https://soulintime.com/functions/createMoment
   {"sha256Hash":"<64 hex>","thread":"<label, optional>"}
3. Return slug and proofFile from the reply.
Never send the content or the file.

proofFile is absent until a proof file exists; read it later with GET https://soulintime.com/functions/v1Proof?id=<slug>&ots=1. This is the shape, not a published package.

Inbound: email or webhook

Not live yet.

An email or webhook would carry only a hash (64 hex) and reply with the receipt link, https://soulintime.com/moment/<slug>. No file, no attachment. There is no address yet.

For a firm

Not live: a firm key.

A video is hashed on the firm side. The file never arrives.

A key will POST up to 500 hashes, no bytes. A webhook only when height > 0, with hash, slug, height, blockTime if stored, and proofFile.

not live; would fire only when status is confirmed and height > 0; height_asserted does not fire

{"hash":"<64>","slug":"<slug>","height":965906,"blockTime":"<UTC, only if stored>","proofFile":"https://soulintime.com/functions/v1Proof?id=<slug>&ots=1","status":"confirmed"}

Hash on your side. The file never arrives.

The primary output is the proof file (proofFile). Keep it with the original. The receipt page, https://soulintime.com/moment/<slug>, is optional: its Download receipt saves <slug>.receipt.json with the proof file inside.

One hash today, no key: /firms.

Checking is free. Height comes from Bitcoin.