A contract engine that produces signed, immutable, portable records of bilateral business actions. The trust properties are mechanical, not social — durable evidence that an agreement happened, and what it said when both parties signed.
PAT is a contract engine that produces signed, immutable, portable records of bilateral business actions. Two parties agree to something — a sale, a rental, a delivery, a service milestone — and the system records that agreement in a form that neither side can later alter and either side can carry with them.
The architecture is small and deliberate. Identities are Ed25519 keypairs held in the user's browser. Contracts run inside a sandboxed WebAssembly engine. Records are stored as content-addressed DAG-CBOR blocks in a CAR file, indexed by SQLite. Sites federate through a mesh overlay, with a trust root that signs which contract code is permitted to run on the network.
The trust properties are mechanical, not social. The system does not enforce agreements or replace them. It produces durable, portable evidence that an agreement happened — and what it said when both parties signed.
This document is layered. The first sections explain the system architecturally for readers evaluating PAT as a product or platform. Later sections go deeper for developers and auditors. The final section names what PAT is not, because the framing matters.
The atomic unit of the system is a PAT — a Peer-Attested Transaction. Every meaningful action on the network is one: a contract creation, a counterparty accepting, an item handover, a milestone sign-off, a payment attestation, a return.
When a user signs a PAT, they are signing a canonical string of seven fields:
PAT:v3:{action}:{actor_did}:{hex(payload)}:{nonce}:{timestamp}:{module_cid}
Each field is there for a reason.
action — the verb. What is being done.actor_did — who is doing it. A did:key:z6Mk... derived directly from the actor's Ed25519 public key.payload — the structured data of the action, hex-encoded.nonce — prevents replay.timestamp — when the actor claims this happened.module_cid — the content identifier of the WebAssembly binary that processed this action.The last one is the unusual choice. The signature does not just commit to what the actor did; it commits to which version of the rules governed it. A node running modified business logic produces a different module_cid, and any peer that ingests the resulting action detects the mismatch immediately. The execution environment is bound into the chain.
The signature itself is Ed25519. Verification is strict (verify_strict) — malleable signatures are rejected. The signed PAT, together with linkage fields, becomes an action block: DAG-CBOR encoded, content-addressed by CIDv1(SHA256(DAG-CBOR(record))). Changing any byte changes the CID. Every node can recompute and verify every block independently.
This is the foundation. Everything else in the system is built on the property that an action block, once written, is a permanent and verifiable record of a specific actor agreeing to a specific thing under a specific set of rules.
Not every field in a payload needs to be visible to everyone. Some are payload commitments — fields where what is signed is not the value itself but a salted hash of it, so the value can be selectively disclosed later without affecting the signature or the chain. Section 6 covers this in full.
Action blocks are not isolated. They link into three parallel chains, each proving something different.
prev_cid, prev_actor_cid, and prev_item_cidEach action on a given contract links to the previous one via prev_cid. This gives the contract a total order of agreed actions. Since prev_cid is part of the signed block, changing any earlier action invalidates every CID that follows.
Each action a user takes links to their own previous action — on any contract, anywhere in the network — via prev_actor_cid. This proves a user's actions are consistent with each other. If Alice signs two different actions both claiming the same prev_actor_cid, that is a provable fork: she signed two conflicting continuations of her own history. The system detects this automatically and freezes the affected contracts until the conflict is resolved explicitly. Time-of-signing is not used to break the tie; the chain structure is the only ordering authority.
Items have their own chain. Each state change — a custody transfer, an ownership change, an attestation about condition — produces a new item block linked to the previous by prev_item_cid. The item's identity is stable; its state at any time is the head of its chain.
Together these three chains let any observer answer three different questions from the same body of records: what happened on this deal, what has this person done, and what has happened to this thing.
A user's identity is their Ed25519 keypair. There is no account, no profile owned by a site, no central registry to enrol with. The public key, encoded as did:key:z6Mk..., is the user's name in the system.
The private key never reaches any server. It lives in the user's browser, inside a capsule encrypted with a key derived from their password via Argon2id, and sealed with XChaCha20-Poly1305. The capsule is stored in IndexedDB. When the user signs a PAT, the browser decrypts the capsule locally, signs, and sends only the signed payload. A server compromise does not expose user keys, because the keys are never there to expose.
Public keys propagate across the federation as a side effect of activity. When Alice acts on a contract at Site 1 and that action syncs to Site 2, Alice's DID arrives with it. Sites maintain a local DID registry; there is no home site for an identity. A user can move between sites simply by acting there.
PAT ships with eight contract types. Six are peer-to-peer — both parties are humans or businesses acting as counterparties. Two are user-petitions-system contracts, where a user requests something of a site's administration.
| Type | What it covers |
|---|---|
pos | Point of sale — direct item purchase between seller and buyer. |
consignment | Item held by a consignee (typically a shop) and sold on behalf of the consignor (the owner). |
delivery | Logistics — moving items through a driver from sender to recipient, with custody tracking at each stage. |
rental | Time-bound item use with deposit, billing periods, late-fee logic, and return attestation. |
service | Milestone-based service delivery between provider and client, with milestone sign-off and unit consumption. |
purchase_order | B2B procurement — buyer issues a PO, seller fulfils, with escrow-backed payment and dispute resolution. |
| Type | What it covers |
|---|---|
mint | A requester asks for tokens; a site admin approves or rejects. |
burn | A requester destroys tokens; a site admin counter-signs to confirm. |
These two are structurally different from the peer-to-peer types. The requester's signature initiates the contract; the admin's signature on the approving action closes it. The admin cannot mint tokens to an arbitrary account on their own — the requester_did is locked into the contract at creation, so the entire lifecycle requires both parties.
Contract types are TOML definitions compiled into the WebAssembly binary at build time. The binary itself has a CID — the same module_cid that appears in every PAT signed by that build. Every action block carries the module_cid of the binary that produced it.
This means a contract type, as it existed when an action was signed, is permanently bound to that action. If the rules change, a new binary is produced with a new module_cid. Future actions carry the new CID; old actions retain their original one. There is no migration of existing instances. History is sealed.
Not every part of every contract should be visible to every observer. The agreed price of a sale is between the parties; the existence of the sale may need to be visible to a regulator; the customer's contact details should be visible to neither. PAT supports this without weakening the signature: every field is committed by the same signed record, but the plaintext of a field can be held back and revealed only to whoever should see it.
The mechanism is a commitment scheme. For fields that are not in the clear, the signed payload contains a salted hash of the value rather than the value itself. The signature covers the hash, the hash covers the value, and the salt prevents an observer from guessing the value by enumeration. Later, holding (salt, value) is enough to prove to anyone that the committed field signed at that moment had exactly that value — without needing the signer's involvement, a decryption key, or any new trust ceremony.
Every field in a PAT payload belongs to one of three tiers:
| Tier | What is signed | Who can prove the value |
|---|---|---|
| Open | The plaintext value | Anyone who has the record |
| Provable | A salted commitment | Either party to the contract — both hold (salt, value) |
| Private | A salted commitment | Only the originating user — only they hold (salt, value) |
The Open tier is the system's default and covers the majority of fields. Routing information, contract metadata, item identifiers, action types — anything that needs to be readable for the system to function is Open.
The Provable tier is the agreement layer. Price terms, billing periods, deposit amounts, payment schedules — fields that are the substance of what the two parties agreed to. Both parties hold the salt and the value from the moment of signing, so either of them can later prove the exact value to a third party — an auditor, a dispute resolver, a regulator — without the other party's cooperation.
The Private tier is the asymmetric case. A field can be signed and committed by one party, with only that party retaining the salt. The counterparty knows the field exists and was signed over; they cannot themselves prove what it was. This is for things a user wants on the record — provable to themselves, disclosable on their own terms to an auditor of their choosing — but does not want their counterparty able to re-disclose downstream.
The commitment of a single field is computed as:
commit_field(salt, name, value) =
SHA2-256("pat-skfield-v1" ‖ salt ‖ len(name) ‖ name ‖ len(cbor(value)) ‖ cbor(value))
A 32-byte digest. The signed payload carries a map of field-name to commitment for the non-Open fields, alongside the plaintext for the Open ones. The CID of the action block is computed over the whole structure, so the commitment bytes are cryptographically bound to the signature.
This is the entire cryptographic mechanism. There is no new signature, no zero-knowledge proof system, no separate key infrastructure. Just SHA2-256 over a domain-separated preimage, with a random salt for confidentiality.
The system's contract types specify their own Provable fields. The selection follows a single principle: do verifiers need this field to route, or is it between the parties?
| Contract type | Provable fields |
|---|---|
rental | deposit_amount, rent_amount, billing_period_days, late_fee_rate, security_deposit_terms, payment_schedule |
purchase_order | agreed_price, payment_terms |
service | agreed_rate, payment_schedule, milestone_amounts, dispute_resolution_terms |
delivery | deliver_to |
pos, consignment, mint, burn | None — all Open |
Point-of-sale transactions need no Provable tier because the amounts are already public in the token transfer they record. Delivery commits the destination address — the sender and recipient both know where the parcel is going, but the routing network in between does not need to. Consignment, mint, and burn don't carry party-to-party terms that warrant withholding from the signed record.
Private-tier fields are not yet enumerated in the standard contract types and are introduced per implementation as customer needs arise. The mechanism is the same as Provable; only the salt-retention policy differs.
When a party wants to prove the value of a committed field to a third party — an auditor, dispute resolver, regulator — the flow is:
(salt, value) from their own store.(salt, value) to the verifier, alongside the original signed action block.commit_field(salt, name, value) and checks it matches the commitment in the record.That last check is the one that matters: the signature was over the committed payload at signing time. Once it verifies, the disclosure is complete. The signer has already attested to the value cryptographically; the discloser is just revealing what was attested.
No interaction with the original signer is required. The verifier learns only the fields they were given (salt, value) for; every other committed field in the same payload remains hidden. The signature is one check that covers all fields at once — disclosure is arithmetic, not a new trust ceremony.
Open fields travel in the signed action block and replicate across the federation through normal sync. They are visible to every node that receives the block.
Provable and Private fields also travel in the signed action block — as commitments only. The action block is the same shape; the federation does not need special handling for committed payloads. What differs is where the plaintext and salt live:
(salt, value) from the moment of signing. Either can disclose.(salt, value). Disclosure is unilaterally theirs.Plaintext disclosure to a third party is always out-of-band: a separate channel from the signed record itself. The chain proves what was committed; the off-chain disclosure proves what the commitment was committing to. The two together prove the field's value at the moment of signing — and nothing else.
Items are the nouns of the system. Actions are the verbs. A contract is a structured sentence about both.
An item — a motorbike, a yoga class slot, a bag of coffee being delivered, a piece of inventory being consigned — is an identifier with a chain of state. The identifier is stable across the item's entire life. The chain records every action that has touched it.
An item carries two relational fields that are conceptually distinct:
holder_did — who has physical custody right now.owner_did — who has legal ownership.A rental moves the holder without moving the owner. A sale moves both. A consignment splits them: the consignee holds the item; the consignor still owns it. The system represents these as plain state on the item chain, advanced by signed contract actions.
Items do not move in a database-delete sense. They acquire new chain heads at new locations. When Bob purchases an item from Alice at Site 1 and later takes a related action at Site 2, the action block from Site 1 propagates to Site 2 through federation sync. Both sites now hold a copy of the relevant history. The item's current state is whatever the latest block in its chain says — and that latest block is reachable from any site that has been synced.
Two sites can transiently disagree about an item's state when one has seen actions the other has not. Sync resolves it: the lagging node pulls the missing blocks and walks the chain forward. For genuine conflicts — where an actor produced two competing continuations off the same prev_actor_cid — the fork-detection system records both branches in actor_chain_forks, freezes affected contracts, and requires an explicit cancel_fork_branch action to resolve.
PAT runs on three coordinated runtimes: the browser, the host, and the WebAssembly guest. Each has a deliberate, narrow role.
The browser is where keys live and where signing happens. It also runs the same WebAssembly engine the server runs (pat_core::engine::embedded_registry()) so that previews and validation behave identically to the server's execution. This is not a coincidence; it is enforced by single-sourcing the engine.
The host is a Rust binary running a wasmtime runtime. It owns storage — the blocks.car file and the SQLite index — and exposes a narrow surface to the WebAssembly guest through WIT (WebAssembly Interface Type) bindings: storage reads and writes, item operations, token operations, a clock, DID public-key lookups. Nothing else.
The guest is the contract engine, compiled to WebAssembly. It receives a signed PAT, calls the host through WIT to fetch the actor's public key, reconstructs the canonical signing string, and verifies the signature using strict Ed25519. It then runs the contract's state machine and produces the resulting action block. The guest never signs anything. Key material does not cross into the WebAssembly sandbox.
Three reasons, in order of weight.
Determinism. The same WebAssembly binary, given the same input, produces the same output on every node. Any node can re-execute an action and check that it produces the expected CID. Because module_cid is part of every signed PAT, a node running modified rules is detectable by any peer the moment one of its actions arrives.
Sandboxing. The guest can call only what the host explicitly exposes. It cannot reach the network, the file system, or cryptographic key material. The contract engine is isolated from infrastructure concerns.
Portability. The same contract code runs in the browser and on the host. A client previewing a contract in their browser sees the rules apply exactly as they would when the action is finally signed and submitted.
A fourth property falls out of the split rather than being designed in directly. A block arriving from another node is checked at three distinct layers, none of which duplicate the others: the TLS handshake proves the peer's iroh NodeId matches its claimed DID; CID verification proves the bytes match their content identifier; signature verification proves a specific user's key signed the contained action. Transport, content, and application checks each catch a different failure mode, and the architecture makes all three cheap to perform.
Every action block, item block, and blob produced by a node is appended to a single blocks.car file in the node's data directory. CAR — Content Addressable aRchive — is a standard IPLD format: a flat sequence of (CID, bytes) pairs.
SQLite sits alongside as an index, not a source of truth. The car_index table maps cid_bytes → (offset, length) into the CAR and is declared WITHOUT ROWID with cid_bytes as the primary key — a clustered B-tree keyed on binary CID bytes. The actions, items, checkpoints, and other tables are derived views — they can be rebuilt entirely by replaying the CAR. If the SQLite database is lost, the CAR rebuilds it. If the CAR is lost, the SQLite database is no longer trustworthy.
Reads do not walk the CAR. A block fetch is an O(log N) B-tree lookup in car_index followed by a single pread() against the CAR at the recorded offset. A 100 MB CAR and a 10 GB CAR are identical in lookup cost. Sequential scans of the CAR happen only during crash-recovery index rebuilds and offline audit tooling, never on the hot read path.
Because every action block is self-describing DAG-CBOR with a content-addressed CID, the format itself supports verification by any conforming tool. The federation API exposes raw blocks directly:
GET /api/federation/actions/{contract_id}
→ [{cid: "bafyrei...", cbor_hex: "..."}, ...]
A client leaving a site can pull their contract action records via this endpoint, walk the prev_cid links to reconstruct the chain, and ingest the blocks into another PAT installation via POST /api/federation/ingest. The CIDs are standard CIDv1 DAG-CBOR. The blocks are valid IPLD. The data is yours, in a format that outlives any single installation.
PAT's data format is deliberately aligned with the IPFS ecosystem. CIDs are valid CIDv1, the codec is DAG-CBOR (0x71), the hash is SHA2-256, and blocks are stored in CARv1. Hand any PAT block and its CID to a standard IPFS tool and the hash validates. The format work is done.
What is not done is the network. PAT does not participate in the IPFS network: no Bitswap, no DHT, no provider records, no gateway. A typical ipfs get <cid> does three things in sequence: it queries the DHT for providers of that CID, fetches the block via Bitswap, and verifies the hash. A PAT block passes the third step perfectly. The first two fail — no PAT node publishes provider records to the DHT, and no PAT node speaks Bitswap. The IPFS network has no way to discover or fetch PAT content because PAT announces nothing to it.
The reason is specific and not philosophical. PAT's QUIC layer is built on iroh, whose block-transport component (iroh-blobs) content-addresses with BLAKE3 rather than SHA2-256. Using it for wire-level addressing while preserving SHA2-256 CIDs would mean dual-hashing every block on every write and read. Switching PAT's CIDs to BLAKE3 instead would invalidate every signature in every history on every node, because signing payloads embed CIDs throughout. Both costs are large enough that PAT runs its own thin block-sync protocol over QUIC instead.
A read-only HTTP gateway exposing blocks by CID would be technically straightforward — the index already supports it. The decision not to expose one is architectural: PAT's cross-site information flow is signed envelopes over QUIC, and adding an HTTP surface for content would weaken that boundary without serving any current need.
The practical position is this. The verification property is real and usable today: an auditor can extract a block from PAT via the federation API and confirm with standard IPFS tooling that the bytes hash to the claimed CID. The workflow is extract then verify, not discover and fetch. Fetching blocks from the network requires PAT-specific tooling. If full IPFS interoperability ever becomes a requirement, the path to it is known and the cost is understood. It is not closed off; it is deferred deliberately.
PAT can run as a single isolated node — a business that wants the contract engine and nothing else. The federation is what those nodes opt into when they want their records to be verifiable and exchangeable with other businesses on the network.
The authority is the trust root, not a gatekeeper. Specifically, it holds the canonical shared state of the network — the registry of sites, members, DIDs, and blessed WebAssembly modules — and it operates the signing key for the network allowlist.
It does not approve individual transactions. Site nodes process user actions fully locally: signature verification, state machine execution, storage. The authority is never in the path of a live transaction.
The authority also performs audit sampling — roughly 5% of cross-site actions are re-verified by the authority as a network-wide integrity check.
A single architectural property is worth naming on its own. The same 32 bytes of Ed25519 public key serve simultaneously as:
did:key:z6Mk... for signing PATs and contractsNodeId for QUIC TLS authenticationThe encoding bridge between them is mechanical (one module in pat-core converts between the two representations) but the cryptography is identical. This means the QUIC TLS handshake is the DID authentication. When Site 1 dials Site 2, the TLS handshake completes with a cryptographic proof that the peer holds the private key matching a specific Ed25519 public key — and that key is checked directly against the site's DID in the local registry. There is no separate API token, federation credential, or service-account layer.
A node's network identity and its contract-signing identity are the same identity. The cryptographic assertion behind one is the cryptographic assertion behind the other.
The federation runs over iroh: a gossip overlay for announcements and direct QUIC connections for block transport. The recurring HTTP polling that earlier versions used has been removed.
Gossip uses Plumtree/HyParView (via iroh-gossip) and carries all federation announcements as DAG-CBOR messages, capped at 64 KB per message:
AnnounceHead — a new action committed on a given contractAnnounceActorHead — a user's actor chain has advancedAnnounceTombstone — a signed deletion has occurredAnnounceAllowlistRevision — a new blessed-module list is availableAnnounceContractFrozen — a cross-site freeze markerAnnounceSiteDirectoryRevision — directory state has changedHlcSync — periodic logical-clock mergeTwo topic classes are used: one global topic for network-wide announcements, and one per-user topic for actor-specific updates. The authority is the bootstrap entry point — the first node a new site dials to join the mesh — but it is not in the path of each gossip message. Once a site's HyParView view has formed, announcements flow through whatever neighbours the protocol established.
Block sync uses a CBOR-framed QUIC protocol identified by the ALPN /pat/sync/2. Any node dials any other directly by iroh::PublicKey. The available requests include GetActionsSince, GetActorHead, GetAllowlistRevision, GetEffectsForDid, and GetSiteDirectoryRevision. A legacy JSON variant (/pat/sync/1) still exists for backward compatibility and is scheduled for retirement once all consumers migrate.
Node discovery is via Pkarr — public-key-based DNS, where each node publishes its current QUIC address under its own public key. Any node can resolve any other node's address from its NodeId without consulting the authority.
The remaining HTTP residual. One HTTP call from a site to the authority survives: a one-shot bootstrap (GET /api/federation/site-bootstrap/:site_id) that runs once when a site starts without a local site record. It pulls a full site snapshot — members, DIDs, items, allowlist, contract IDs — and is not used again afterward. This is the last federation-side HTTP call, and it has a named migration target: a GetSiteData QUIC frame, scheduled to replace it.
Browser traffic is HTTP and will remain so. Browsers do not speak iroh ALPNs, and the boundary between human-to-machine (HTTP) and machine-to-machine (QUIC) federation is deliberate. The migration is about removing HTTP from the federation wire, not from the system as a whole.
The most consequential thing the authority signs is the network's allowlist of permitted WebAssembly modules. This is what makes the trust property — "all nodes are running rules we have all agreed are valid" — enforceable.
The lifecycle, end to end:
SHA256(binary) → CIDv1 and that is the binary's module_cid.SignedAllowlistRevision — a DAG-CBOR record listing the approved module_cids with their version and status, monotonically increasing rev_seq, the admin's DID, and an Ed25519 signature over the canonical encoding. Signing happens on the admin's machine. The private key never leaves it.verify_strict, stores the revision idempotently, and rebuilds its materialised blessed_modules table to reflect only the active entries from the latest rev_seq.AnnounceAllowlistRevision message on the global topic. Every connected node hears it.rev_seq against its own. If behind, it dials the announcing peer over /pat/sync/2, fetches the revision, verifies the signature against its locally-configured network admin DID, and atomically updates its own blessed_modules.SiteBootstrapResponse, so they have a populated blessed_modules table before any module-bearing action sync can arrive.module_cid is in the local blessed_modules table. If it is not, the node refuses to start. There is no auto-bless, no fallback, no per-site override. A test-only environment variable exists for development and explicitly panics if used in production.module_cid is checked against blessed_modules before being accepted into storage. An empty allowlist fails closed.This is the only place the network's trust depends on a privileged signer. Everything downstream — contract execution, signature verification, fork detection, chain integrity — is mechanical and runs on every node independently.
Consign1 is the current operator of the authority and the holder of the network admin signing key. This is structural, not a product tier: someone has to sign which code is allowed to run, and someone has to host the bootstrap node that new sites first contact. Consign1 does that work.
The authority is not a profit centre. PAT is free; running it standalone is free; joining the federation is free. Consign1's commercial model is implementation and support of installations, not gatekeeping access to the network. But the authority is a real position, and naming it plainly in the technical documentation matters more than dressing it up.
A few framings the system invites and does not deserve.
This is not a cryptocurrency. There are tokens in the system, but they are not money and they are not transferable for value outside a contract. A token in PAT records an amount that two parties have attested to inside a specific transaction — the agreed price of a sale, the agreed deposit on a rental, the agreed payment for a milestone. What matters is that the values inside a contract match what the parties agreed to. The number of tokens any user "holds" outside the bounds of a contract is not meaningful.
This is not a blockchain in the speculative sense. There is no global consensus, no proof-of-anything, no mining, no shared state machine that the whole network agrees on. Each contract is its own small chain. The cryptography produces evidence, not consensus. Sites do not compete to write the next block; they each write their own blocks and share them.
This is not a legal contract system. A signed PAT is not a substitute for a legal agreement and the system makes no claim of legal enforceability. What it produces is durable, portable, tamper-evident evidence of a mutual agreement. Whether that evidence is useful in a legal context is a question for the jurisdiction and the parties involved.
This is not a platform that takes custody. Identities live in the user's browser. Records live in CAR files that the holder can extract. There is no account a user must keep with anyone. The system is designed to be exited as easily as it is joined.