Integration guide
How to make a BitGraph from your own code, keep what comes back, and check it later. Written for an engineer doing a first integration: one operation, shown from the CLI, the SDK and two HTTP calls, all producing the same proof on the same sequence. Connecting an AI agent instead? See the MCP server.
What you need
- RuntimeNode 20 or later for the CLI and the SDK. The HTTP calls need only an HTTP client and a SHA-256 implementation.
- Packages
@mikeargento/bitgraph-sdk0.4.1 makes a BitGraph and ships thebitgraphcommand.@mikeargento/bitgraph-verify1.16.0 checks one; it is MIT-licensed and makes no network call.@mikeargento/bitgraph-audit0.9.0 is the deeper offline audit. The older@mikeargento/bitgraphpackage made the earlier form (set/1, set/2, the Frame file); what it made still verifies, but new integrations use the SDK. - AccountNone. The public endpoint at
https://bitgraph.ingneeds no key. A self-hosted enclave may require a Bearer token; see the API reference. - The fileStays on your machine. What crosses the network is digests, the tree’s 84-byte root document, the signed position record, and each file’s sealed recovery entry. That is a property of each client on this page; the enclave never receives a file.
- The specSPEC.md is the normative text. Every proof pins the SHA-256 of the version it was made under, and the holder keeps a copy beside the export.
Words used on this page
- File
- Any bytes.
- Digest
- The file’s SHA-256, 32 bytes, standard base64 inside a proof.
- Position
- A place in the enclave’s sequence, allocated and signed before it receives any digest. The API calls its signed record a slot (
slot,slotId). - Commit
- The single step that binds a digest to the position and consumes it. Atomic: either a proof exists or nothing does.
- Proof
- The signed
bitgraph/1JSON. It travels inside the export. - Tree
- What one BitGraph is: a
tree/1Merkle tree of files under one position (SPEC section 8). Each file is a leaf; one file is a tree of one. - Export
- The
bitgraph-export/1JSON the holder keeps beside the files, with SPEC.md: the proof, the tree evidence, the floor header, and the ceiling and settlement once they exist. It holds no copy of any file. - Committed bytes
- New bytes built around a file that carry a position commitment. Virtual: rebuilt from the file and the proof whenever needed.
- Floor
- The newest Base block when a position opened, bound into it by the enclave and signed into the proof as
commit.slotFloor: the record was made after it, a time. Earlier proofs stand on an Ethereum anchor instead (commit.slotAnchor), a position whose file was an Ethereum block hash. - Epoch
- One enclave lifetime, one UTC day in production. The signing key is destroyed at the end of it.
The recommended path: make a BitGraph
Making a BitGraph is one operation with four steps, and the order is the point.
- Reserve a position. The enclave allocates an unused position in its sequence and signs a position record for it before it receives any digest. The record has no field that could hold one.
- Build the committed bytes. Derive the 32-byte position commitment from the signed position record and its floor block, and write it into new bytes for each file at a registered placement. The original is never modified.
- Build the tree. Each file becomes a 65-byte leaf (placement, the committed bytes’ digest, the original’s digest). The leaves form a Merkle tree, and an 84-byte root document holds the count, the root and the commitment. One file is a tree of one.
- Commit under the same position. The digest sent is the SHA-256 of the root document. The enclave binds it to the position and consumes it in one atomic step, signs the body, and attests it. If any part fails, no proof exists and the position is lost.
The commitment is a function of a record that did not exist until the position was allocated, so the committed bytes could not have been finished before that moment, and the floor the enclave fixed at that moment puts a public time under them. That bound reaches the committed bytes. It does not reach the original: the original can be any age, and the proof says only that it existed no later than the commit. A file recorded as is (--as-is) has the record floor only: recorded after the block, its bytes themselves not dated.
A record you produce yourself
The four steps above wrap a file that already exists. A format you write yourself, such as an AI system’s audit record, carries the commitment in a field of its own instead, and the order changes: the commitment goes inside the record before the record is signed, so the finished record depends on a position that existed before it.
- Take a position. The system asks BitGraph for a position and receives its position commitment.
- Put the commitment in the record. It is written into the record like any other field, before signing.
- Sign the record. Only now does the record’s final fingerprint exist.
- Commit the fingerprint to the same position. Within the 120-second window the position is consumed and the proof binds the two.
The signed record now contains a commitment to a position that existed before the record’s own fingerprint, and that position is committed by this exact record. The record could not have been finished before the position, and it cannot be moved to another place afterwards. An agent connected over MCP does this for its own task records.
What comes back
- The proofAn ordinary
bitgraph/1proof whose artifact digest is the SHA-256 of the root document.slotAllocationis the position record you held,commit.slotCounterits counter,commit.counterthe commit position, and the signedattributionis the marker: namebitgraph-fuse/3(bitgraph-fuse/2on earlier proofs), titletree/1, message the SHA-256 of SPEC.md. The root document rides unsigned inmetadata["bitgraph-tree/1"]as hex and must hash to the signed digest. - The exportFrom the CLI and the SDK,
bitgraph-<counter>.bitgraph.json, abitgraph-export/1file holding the proof, the root document, every leaf and name, and the floor header. A member export (<name>.bitgraph.json) holds one file’s leaf and path instead. SPEC.md is written beside them. The Base ceiling and its settlement are pending at first;bitgraph export completefills them in later. - The committed bytesVirtual. Nothing is written beyond the export; the original plus the proof rebuilds them byte for byte whenever they are needed.
- Recovery entriesEach file gets sealed entries under names derived from its hash, so the file alone can find its proof again if the export is lost. The hash itself is never indexed.
--no-recoverykeeps none.
From the command line
npx @mikeargento/bitgraph-sdk record photo.jpg --out ./proofs # one tree; writes ./proofs/bitgraph-<counter>.bitgraph.json and SPEC.md beside it # --exports owner|members|both|none, --as-is, --again, --no-recovery # Check the file against its export, offline: one line per claim npx @mikeargento/bitgraph-sdk verify photo.jpg ./proofs/bitgraph-<counter>.bitgraph.json # exit 0 TRUE, 2 FALSE or corrupt; --eth-rpc and --base-rpc confirm the blocks against nodes # Later: add the Base ceiling and its Ethereum settlement once they exist npx @mikeargento/bitgraph-sdk export complete ./proofs/bitgraph-<counter>.bitgraph.json
Placements: trailer/1 for formats whose decoders ignore trailing bytes (JPEG, PNG, GIF, TIFF and TIFF-based raws, BMP, RIFF such as WebP), container/2 for everything else (a tar with the original first; the older container/1 stays readable). The SDK chooses the placement from the bytes. The byte layout of each placement is on the proof format page and in SPEC.md section 7.
From the SDK
The same four steps from code:
import { BitGraph } from "@mikeargento/bitgraph-sdk"; const bg = new BitGraph(); const r = await bg.record(["photo.jpg", "notes.txt"], { exportDir: "./proofs", exports: "both" }); r.files[0].proofUrl; // the record's public receipt r.made?.exports?.owner; // ./proofs/bitgraph-<counter>.bitgraph.json: keep it with the files // Later, offline: the file with its export, one line per claim await bg.verifyExport("./proofs/bitgraph-<counter>.bitgraph.json", "photo.jpg"); // Later, online: add the Base ceiling and its settlement once they exist await bg.completeExport("./proofs/bitgraph-<counter>.bitgraph.json");
Without exportDir nothing is written and r.made holds everything an export is built from. Files already on record come back as on record, untouched, unless you pass again.
Over HTTP
The same two calls are POST /api/fuse/allocate and POST /api/fuse/commit on https://bitgraph.ing. Over HTTP you build the committed bytes, the leaves and the root document yourself: the commitment, the placements and the tree are specified in SPEC.md sections 6 to 8, and the SDK is a reference implementation.
# 1. Reserve a position. No body. The response carries its signed record # ("slot"); slotId is its nonce, a bearer ticket until it is consumed. curl -X POST https://bitgraph.ing/api/fuse/allocate # { # "slotId": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=", # "slot": { "version": "bitgraph/slot/1", "nonceB64": "...", "counter": "277", # "epochId": "...", "publicKeyB64": "...", "chainId": "bitgraph:main", "signatureB64": "..." }, # "chainId": "bitgraph:main", # "floor": { "chain": "base", "evmChainId": 8453, "blockNumber": 52271417, # "blockHash": "0x...", "blockTimestamp": 1791332181 } # the Base block the commit will sign # } # 2. Build the committed bytes, the leaves and the 84-byte root document from # that record and the floor's blockHash (SPEC.md section 8), then commit the root document's hash under # the same position within 120 seconds. Exactly one digest per commit. curl -X POST https://bitgraph.ing/api/fuse/commit \ -H "Content-Type: application/json" \ -d '{ "slotId": "<slot.nonceB64>", "slot": <the position record from step 1, verbatim>, "digests": [{ "digestB64": "<SHA-256 of the root document, base64>", "hashAlg": "sha256" }], "chainId": "bitgraph:main", "attribution": { "name": "bitgraph-fuse/3", "title": "tree/1", "message": "<SHA-256 of SPEC.md, base64>" }, "floor": <the floor from step 1, verbatim>, "metadata": { "bitgraph-tree/1": "<the root document, 84 bytes as hex>" } }' # { "proof": { ... } } an ordinary bitgraph/1 proof, committed under the position you reserved
The route checks the marker, that it knows the spec hash, the exact metadata shape, the root document’s commitment against the named position and floor, and its hash against the digest, all before the position is spent. The enclave signs the floor it fixed when the position opened, so a commitment built on any other block fails verification. A commit that fails is reported as a failure; it is never downgraded to an ordinary recording, and the route refuses to return a proof minted under any position other than the one you named. The earlier single-file marker (bitgraph-fuse/1, title a placement) is still accepted. Every request, response, status code and error is in the API reference.
An export and recovery entries are the client’s work: the export is built from the proof and the tree you hold (SPEC.md section 12), and entries are written with POST /api/recovery and read back with GET /api/recovery/<address> or POST /api/recovery/lookup (section 13). Check them before making anything again: a file already on record is found, not made twice.
What you send and what you get back
| Step | You send | You get back |
|---|---|---|
| Allocate | Nothing. An empty POST. | slotId (the nonce), slot (the signed record), chainId. |
| Commit | slotId, the slot record verbatim, the root document’s digest, the marker attribution, and the root document in metadata. | { proof }: the bitgraph/1 proof of the tree. |
| CLI or SDK | One or more files. | The export bitgraph-<counter>.bitgraph.json with SPEC.md beside it, and recovery entries written for each file. |
What to store
Keep the export and SPEC.md beside the files. Keep the files unchanged: the files plus the export are the durable state, and the committed bytes are rebuilt from them whenever someone needs to check them. An export holds no copy of any file and no anchor proofs; a tree/1 proof signs its own floor.
The export returned at commit time is the record. The service keeps a copy of each proof and indexes it by digest as a convenience, but that copy is not the evidence, and a lookup that finds nothing is not evidence that bytes were never recorded. Store what comes back.
Recovery is the net under a lost export. Each file’s sealed entries sit under names derived from its hash, encrypted with a key derived from it too, so whoever holds the file can find and open them and nobody else can. The SDK, the CLI, the MCP server and the drop box consult them before anything is made again. Recovery is a convenience BitGraph runs; the export is the record.
Never write the raw slotId into a file or a log. It is the position’s nonce, and until the commit it is a bearer ticket; the file carries only the derived commitment. Store the last accepted commit.counter for each epoch you have seen, so a replayed proof from an earlier position is noticed.
How to verify
Verification needs the evidence and an explicit trust policy. The evidence is the export (the proof, the tree evidence and the floor block’s header), and the file (the original, or the committed bytes built from it). The policy is an allowlist of enclave measurements (PCR0 values) you accept, and a requirement that the hardware attestation be present. Without the allowlist, a proof from any enclave image would pass. A proof that pins a spec hash the verifier does not know is answered undetermined, never TRUE.
A file with its export: verifyExport
The current form. One line per claim, each saying what it rests on, and three time claims never merged: the floor (a Base block, or an Ethereum block on earlier proofs; the record came after it), the ceiling in time (a Base block; the record existed by it) and its settlement (Ethereum’s own record of that Base block). Order after the record, the next BitGraph in the chain, is a bound in position, never a clock time.
import { promises as fs } from "node:fs"; import { parseExport, verifyExport } from "@mikeargento/bitgraph-verify"; const exp = parseExport(await fs.readFile("bitgraph-4821.bitgraph.json", "utf8")); if (exp === null) throw new Error("not a bitgraph-export/1 file"); const result = await verifyExport(exp, { bytes: await fs.readFile("photo.jpg"), // the original or the committed bytes; without it the file claims are NOT_CARRIED pins: { pcr0: ["5a947cc66095adcceefa9e5ece5d1416dfe08c3470df1bcaa5b2bc5267b0480e6cdc172fe077cd06b0afb07614307973"] }, // enclave-v10; default is BitGraph's published images }); result.verdict; // "TRUE" | "FALSE" | "UNDETERMINED" result.claims; // one per claim: id, result, what it rests on result.times; // floor, ceilingBase, ceilingEthereum, each as established or null result.reading; // plain language, written from the claims
verifyTreeMember from the same package checks one member’s evidence against a proof and its root document when you hold them apart from an export. The same judgment from the shell, with no code of your own: npx @mikeargento/bitgraph-sdk verify photo.jpg bitgraph-4821.bitgraph.json. The deeper audit over a folder of exports is npx @mikeargento/bitgraph-audit.
A plain recording: verify
import { verify } from "@mikeargento/bitgraph-verify"; const result = await verify({ proof: myProof, bytes: artifactBytes, trustAnchors: { requireEnforcement: "measured-tee", allowedMeasurements: ["5a947cc66095adcceefa9e5ece5d1416dfe08c3470df1bcaa5b2bc5267b0480e6cdc172fe077cd06b0afb07614307973"], // enclave-v10, see PINS.md requireAttestation: true, requireAttestationFormat: ["aws-nitro"], }, }); if (result.valid) { console.log("Proof verified successfully"); } else { console.error("Verification failed:", result.reason); }
Pin allowedMeasurements to the published PCR0 for the current enclave (v9, above) and set requireAttestation. The measurement is reproducible from the published source; the self-host page shows how to rebuild it.
An earlier single fused file: verifyFuse
For a single fused file made in the earlier form (marker bitgraph-fuse/1 or /2 with a placement title, a Frame file beside it), use verifyFuse from the same package. It runs the same checks, then compares the commitment: FUSED_DIRECT when the bytes are the fused copy, FUSED_FROM_ORIGIN when they are the original and rebuild the committed file byte for byte, RECORDED for an ordinary proof, NO_MATCH when the bytes match neither digest. See Verification for the full outcome table.
import { verifyFuse } from "@mikeargento/bitgraph-verify"; // bytes: the new file, or the original it was made from const result = await verifyFuse({ proof: frame.proof, bytes, frame }); result.category; // "FUSED_DIRECT" | "FUSED_FROM_ORIGIN" | "RECORDED" | "NO_MATCH" | ... result.span; // { slotCounter, commitCounter, epochId, chainId, positions } result.statements; // the bounded statements, verbatim
Over HTTP
For callers that cannot run a verifier: shell scripts, anything without a JavaScript runtime. The endpoint delegates to the same package, so it and the offline verifier cannot disagree.
DIGEST=$(openssl dgst -sha256 -binary myfile.pdf | base64) curl -X POST https://bitgraph.ing/api/verify \ -H "Content-Type: application/json" \ -d '{"proof": '"$(cat proof.json)"', "digest": "'$DIGEST'"}' # { # "verified": true, # "status": "valid", # "artifactBinding": "checked", # "counter": "7910", # "epochId": "...", # "proof": { ... } # }
Send proof and digest together to check that the proof describes that exact file. Add allowedMeasurements to reject anything not signed by a specific enclave build. The digest may be hex or base64, either form. A digest alone is looked up in the service’s index; a miss there is not a verdict about the bytes, so send the proof when you hold it.
Read artifactBinding, not just verified. checked means the digest you sent matches the one inside the proof. not-checked means the proof is sound but nothing tied it to a file, which is what you get from a proof with no digest alongside it. mismatch means the proof is genuine and is for different bytes. A verdict from the service that issued the proof is a convenience; the proof comes back whole so you can redo the check yourself, which is the result that counts.
The enclave’s key and measurement
# Get enclave public key and measurement curl https://nitro.occproof.com/key # Response: # { # "publicKeyB64": "...", # "measurement": "5a947cc66095adcc...307973", # "epochId": "...", # "enforcement": "measured-tee" # }
Compatibility: bytes that already exist
Use this only when the bytes are already final and cannot be rebuilt around a position: an archive, a signed document, something a third party handed you. It gives those bytes a position, which establishes that the exact bytes existed no later than that position in the sequence. The floor bounds the placement, not the bytes.
Hash your file locally, then send only the digest to the BitGraph endpoint. The enclave allocates a position and commits the digest under it in one request.
# 1. Hash your file DIGEST=$(openssl dgst -sha256 -binary myfile.pdf | base64) # 2. Send to BitGraph endpoint curl -X POST https://bitgraph.ing/api/commit \ -H "Content-Type: application/json" \ -d '{ "digests": [{ "digestB64": "'$DIGEST'", "hashAlg": "sha256" }], "chainId": "bitgraph:main", "metadata": { "source": "my-app" } }'
The same from TypeScript
// Hash locally const bytes = new Uint8Array(await file.arrayBuffer()); const hashBuf = await crypto.subtle.digest("SHA-256", bytes); const digestB64 = btoa(String.fromCharCode(...new Uint8Array(hashBuf))); // Commit through the BitGraph endpoint (with optional attribution) const resp = await fetch("https://bitgraph.ing/api/commit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ digests: [{ digestB64, hashAlg: "sha256" }], chainId: "bitgraph:main", attribution: { name: "Jane Doe", title: "Project Photo" }, metadata: { source: "my-app", fileName: file.name }, }), }); const [proof] = await resp.json(); // proof is a complete BitGraphProof JSON object console.log(proof.commit.counter); console.log(proof.slotAllocation); // the position record console.log(proof.attribution); // signed creator metadata
Name, title and message in attribution are covered by the Ed25519 signature: the proof is detectably invalid if they are altered. They are a claim the submitter made, not a verified identity. metadata is not signed and is advisory.
Several at once
Send multiple digests in one request. The enclave allocates a position and commits each digest sequentially. Still the compatibility path: each digest names bytes that already exist.
const resp = await fetch("https://bitgraph.ing/api/commit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ digests: [ { digestB64: digest1, hashAlg: "sha256" }, { digestB64: digest2, hashAlg: "sha256" }, { digestB64: digest3, hashAlg: "sha256" }, ], chainId: "bitgraph:main", attribution: { name: "Jane Doe" }, metadata: { source: "my-app", batchId: "abc123" }, }), }); const proofs = await resp.json(); // proofs[0], proofs[1], proofs[2] - one per digest
What happens between allocate and commit
A position is held for 120 seconds. Up to 1,000 can be open at once across the whole enclave. Allocating one advances the counter immediately, so a position that is never committed leaves a permanent gap in the sequence. Nothing is written when a position is allocated, so an abandoned position is simply that gap. An enclave restart begins a new epoch and voids every position still open.
Inside that window a caller can hold several positions open and decide which file fills which. What the enclave signs is that it issued the position before it received the digest, and then bound the two. It does not sign that the position was chosen without knowledge of the file. Ordering between proofs comes from the previous-proof hash and the counters, so the gaps abandoned positions leave cost nothing.
Checklist
- Hash locally. The file never leaves your machine; only digests, the root document, the position record and sealed recovery entries are sent.
- Check before you make. Consult the recovery entries first: a file already on record is found, not made twice. The SDK, CLI and MCP server do this on their own.
- Commit through bitgraph.ing. The site endpoints sit behind the floor gate, so every position they issue carries a Base floor. Allocate, build, hash and commit within 120 seconds, and treat any failure as a failure: a tree commit is never downgraded to a plain recording.
- Store the export and SPEC.md beside the files. Keep the files unchanged. The committed bytes are virtual and rebuildable; the export is portable and can also live in a separate system. Run
export completelater to add the ceiling and settlement. - Never expose the slotId. Only the derived commitment goes into the committed bytes; the nonce goes nowhere.
- Verify with a pinned policy.
verifyExportwithpins.pcr0set to the published PCR0, or the default list of BitGraph’s published images. Read each claim, not only the verdict. Verification is offline: the export, the file and the public measurement are enough. - Track counters. Store the last accepted
commit.counterper epoch to notice a replay. - Handle the retryable answers.
503 tee-restartingand503 ledger-unavailablemean try again. On earlierbitgraph-fuse/2commits,409 no-anchor-before-slotand409 floor-mismatchwere final for that position: reserve a new one.
Where next
- SPEC.mdThe normative text: tree/1, the export, recovery, the floor, the ceiling and its settlement.
- API referenceEvery endpoint, request, response, status code and error.
- Proof formatThe bitgraph/1 schema field by field, the signed body, and the fused placements.
- VerificationWhat a verifier checks, in order, and what each result means.
- MCP serverConnect an agent with one URL and let it make proofs of its own files.