Web Contracts

State machines anchored to Bitcoin

A web contract separates logic from data. The contract defines rules in any language. The state, ledger, and trail are always JSON — universal, language-agnostic, verifiable.

Contract
Logic & rules in any language
contract.json
📜
State
Current values, seq & hash chain
state.json
💰
Ledger
Who owns what — balances
webledgers.org
Trail
Bitcoin proof via key chaining
blocktrails.org
The contract is the class. The state is the instance. The ledger tracks ownership. The trail proves it happened.

Architecture

A web contract cleanly separates concerns:

LayerFileMutable?Role
Contractcontract.jsonNoRules & transition logic (immutable once deployed)
Statestate.jsonYesCurrent values, updated each transition
Ledgerledger.jsonYesBalance tracking (webledgers)
TrailblocktrailAppendBitcoin-anchored audit log (blocktrails)

The contract logic is pluggable. State, ledger, and trail are always the same format regardless of what language the contract is written in.

Contract validate() transition() effects() js / sol / py State seq, prev state data hash chain json Ledger balances multi-currency did:nostr json Trail Bitcoin BIP-341 anchor bitcoin

contract.json

The contract declares its profile, language, and source. The logic lives in a separate file written in any supported language.

contract.json
{
  "@context": "https://webcontracts.org/v1",
  "type": "WebContract",
  "profile": "amm.v1",
  "id": "pool-tbtc3-tbtc4",
  "language": "javascript",
  "source": "amm.js",
  "created": 1741737600
}

The source file exports transition functions. Each receives the current state and params, and returns the new state plus ledger effects:

amm.js
export function swap(state, params, ledger) {
  const { sender, sell, amount } = params
  const buy = state.pair.find(u => u !== sell)
  const rIn = state.reserves[sell], rOut = state.reserves[buy]
  const amtAfterFee = amount * (1 - state.fee)
  const out = Math.floor((rOut * amtAfterFee) / (rIn + amtAfterFee))

  return {
    state: {
      ...state,
      reserves: {
        [sell]: rIn + amount,
        [buy]: rOut - out,
      },
      k: (rIn + amount) * (rOut - out),
    },
    effects: [
      { op: 'debit', who: sender, currency: sell, amount },
      { op: 'credit', who: sender, currency: buy, amount: out },
    ]
  }
}

Contract languages

The contract logic is swappable. The pod just needs an executor for the language.

JavaScript
Native on Node.js pods
Solidity
Via solc / EVM in WASM
Python
Via sandboxed runtime

The contract interface is always the same: a function that takes (state, params, ledger) and returns { state, effects }. The language is an implementation detail.


state.json

The state is pure data — no logic. It holds the current values and a hash chain for verification.

state.json
{
  "contract": "pool-tbtc3-tbtc4",
  "seq": 5,
  "prev": "a1b2c3d4e5f6...",
  "updated": 1741737600,

  "pair": ["tbtc3", "tbtc4"],
  "reserves": { "tbtc3": 5000, "tbtc4": 10000 },
  "k": 50000000,
  "fee": 0.003,
  "totalShares": 1000,
  "lpShares": {
    "did:nostr:abc123...": 1000
  }
}

Fields

FieldTypeDescription
contractstringReferences the contract id
seqnumberTransition sequence number, starts at 0
prevstring|nullSHA-256 of previous JCS-canonicalized state
updatednumberUnix timestamp of last transition
...restanyProfile-specific state data

Sequencing

Each transition increments seq and sets prev to the SHA-256 hash of the previous state (JCS-canonicalized). This creates a hash chain that can be verified independently and anchored to Bitcoin via blocktrails.

genesis seq: 0 blocktrail swap state seq: 1 blocktrail add-liq state seq: 2 blocktrail ...

Profiles

Profiles define the shape of state and the expected transitions. A contract declares its profile; the source file implements it.

amm.v1
Automated market maker
State: pair, reserves, k, fee, totalShares, lpShares
Transitions: swap, add-liquidity, remove-liquidity
Formula: x × y = k (constant product)
escrow.v1
Two-party escrow with arbiter
State: buyer, seller, arbiter, amount, status
Transitions: fund, release, dispute, resolve, refund
sale.v1
Fixed-price token sale
State: ticker, rate, supply, sold
Transitions: buy, close
subscription.v1
Recurring payment agreement
State: subscriber, provider, amount, interval, nextDue
Transitions: pay, pause, resume, cancel

Ledger effects

Transition functions return an effects array. The executor applies these against the webledger atomically with the state update.

The contract never touches the ledger directly. It declares intent; the executor enforces it.


Validation

Web contracts use a trust but verify model. There are three roles:

  1. Executor — runs the contract source, validates transitions, updates state + ledger + trail atomically. This is any server (or local process) that can load and run the contract language.
  2. Client — submits transitions as JSON. Can optionally run the contract source locally to preview results before submitting (like simulating a swap to show expected output).
  3. Verifier — anyone. Download the contract source, replay the state chain from genesis, check each prev hash, verify the blocktrail against Bitcoin. If the executor cheated, the chain breaks.

No consensus mechanism. No global blockchain. The executor is efficient (single process), and the hash chain + Bitcoin trail provides cryptographic tamper evidence. Anyone with the contract source can independently verify the full history.


Identity

Parties are identified by did:nostr:<pubkey>. Authentication uses NIP-98 HTTP auth — a signed Nostr event in the Authorization header.

This makes contracts accessible to both humans (via NIP-07 browser extensions) and AI agents (via stored keys). Any HTTP client with a signing key can participate.


Transport

Web contracts are transport-agnostic. The four files can live anywhere:

HostExample
Solid podFull integration — auth, ledger, and trail built in
Web serverAny HTTP server with a contract executor
Git repoState as commits, trail as tags
Local directoryAgent runs executor locally, syncs state

When served over HTTP, the convention is:

PUT  /contracts/{id}/contract.json  — deploy contract
GET  /contracts/{id}/state.json     — read current state
POST /contracts/{id}/               — submit a transition
GET  /contracts/{id}/trail.json     — read audit trail

Transitions are submitted as JSON:

{
  "transition": "swap",
  "params": { "sell": "tbtc3", "amount": 1000 }
}

Agent-friendly

Web contracts are designed for autonomous agents:

  1. Agent gets a nostr keypair (identity)
  2. Agent reads contract.json + source to understand the rules
  3. Agent reads state.json to see current values
  4. Agent can simulate transitions locally before submitting
  5. Agent submits transitions via HTTP + NIP-98
  6. Agent verifies trail against Bitcoin

No browser required. No human approval. Compatible with OpenClaw, nanoclaw, and any agent framework that can make HTTP requests.

An agent can even write a contract — deploy contract.json + source, and other agents or humans can interact with it.


Tradeoffs

Web contracts make a deliberate tradeoff: give up trustless global consensus, gain speed, zero cost, language freedom, and simplicity.

EthereumWeb Contracts
ConsensusGlobal PoS, thousands of nodesNone — single executor, verify via trail
LanguageSolidity (EVM bytecode)Any (JS, Solidity, Python…)
CostGas fees per operationNo gas — execution is free, verification is on Bitcoin
Speed~12s block timeInstant — state updates in a single HTTP round-trip
StateOn-chain, globalJSON — portable, readable, hosted anywhere
ProofBlockchain consensusBitcoin-anchored hash chain — deterministic replay
AccessWallet + RPCHTTP + keypair — humans and AI agents alike

For most real-world contracts — escrow between two parties, a token sale, a subscription, an AMM — you don’t need 10,000 nodes agreeing. You need tamper evidence and verifiability. That’s what the blocktrail provides.


Implementations

ProjectRole
JavaScript Solid ServerImplements payment and AMM profiles (JavaScript)
Web LedgersBalance tracking spec
BlocktrailsBitcoin anchoring spec
NIP-98HTTP authentication