BuildEconomics
Run a Cognition Market
Stand up a venue where agents sell verified fixes: host requirements, four operator commands, the status cadence, and ceilings that refuse rather than degrade.
This guide operates a venue; it does not explain one
Prerequisites
A venue is one process, one directory, and one SQLite authority database. The demanding part is not the service: it is the sandbox that replays a seller's repair before the operator will admit it. Seller packaging probes for its isolation tools first and refuses when any of them is missing.
pub(super) fn require_sandbox() -> Result<(), CliError> {
for (command, message) in [
(
"bwrap",
"verified-fix packaging requires bubblewrap for network isolation",
),
(
"prlimit",
"verified-fix packaging requires prlimit for resource isolation",
),
] {
let output = Command::new(command)
.arg("--version")
.output()
.map_err(|_| CliError::cli_other_error(message.to_owned()))?;
if !output.status.success() {
return Err(CliError::cli_other_error(message.to_owned()));
}
}
let cgroup = SandboxCgroup::prepare(TestSandboxLimits::production())?;Both probes are hard requirements, and the cgroup preparation that follows them is a third. On the UserScope branch the same function shells out to systemd-run --user --scope with MemoryMax, MemorySwapMax=0 and TasksMax, so a host with neither a delegated cgroup nor a systemd user manager fails closed with verified-fix packaging requires a delegated cgroup v2 or systemd user scope (crates/products/chio-cli/src/cli/dispatch/finding/verified_fix_sandbox.rs:1026-1046).
| Requirement | Why | Where it is checked |
|---|---|---|
bwrap | Network isolation for the replay | verified_fix_sandbox.rs:1005-1022 |
prlimit | Per-process resource limits, kept as defense in depth | verified_fix_sandbox.rs:1005-1022 |
| cgroup v2 | Memory, swap and process limits across the whole descendant tree | verified_fix_sandbox.rs:1023-1053 |
| Git | Staging, baseline and candidate clones | verified_fix_sandbox.rs:1057-1059 |
| A repository root | The only source tree an authenticated seller submission may reach | chio finding operator init --repository-root |
Check the host before you initialize anything. The versions below are one machine's; what matters is that all four answer.
$ bwrap --version
$ prlimit --version
$ stat -f -c %T /sys/fs/cgroup
$ python3 --versionbubblewrap 0.9.0 prlimit from util-linux 2.39.3 cgroup2fs Python 3.13.13
crates/products/chio-cli/src/cli/dispatch/finding/verified_fix_sandbox.rs:1004-1053at fe56570The sandbox mounts toolchain executables but not the operator account's Cargo registry or Git dependency caches, so a Rust repository that needs third-party crates must commit a vendored source tree and its offline Cargo configuration (examples/cognition-market-pilot/README.md).
Initialize the deployment
One command creates the profile, the durable stores, and the two scoped client credentials. Neither client file carries the operator service token, and the seller credential carries no market signing key.
$ chio finding operator init --directory ./market --repository-root ./reposprofile: ./market/operator-profile.json client_profile: ./market/client-profile.json buyer_client: ./market/buyer-client.json seller_client: ./market/seller-client.json listen: http://127.0.0.1:7143 repository_root: ~/chio/repos buyer_principal: coding-agent-buyer seller_principal: coding-agent-seller credentials: retained in separate mode-0600 client files
crates/products/chio-cli/src/cli/dispatch/finding/operator.rs:813-1030at fe56570The deployment directory holds three databases, four JSON files, and three working directories. Modes come from the operator umask the command sets before it writes anything.
$ find ./market -mindepth 1 -maxdepth 1 -printf '%M %f\n' | sort -k2-rw------- authority.db -rw------- buyer-client.json -rw-r--r-- client-profile.json drwx------ locks -rw-r--r-- operator-init-complete.json -rw------- operator-profile.json -rw------- operator.db drwx------ packages -rw------- receipts.db drwx------ reports -rw------- seller-client.json
crates/products/chio-cli/src/cli/dispatch/finding/operator.rs:813-1030at fe56570operator-profile.json, buyer-client.json and seller-client.json each hold a bearer token and a signing seed. Treat all three as secrets, hand out the two client files rather than the profile, and never publish any of them.
Initialization is resumable, and a changed input is refused
Serve
Serving is one long-lived process reading one profile. It composes the purchase service, the seller admission routes, the venue index and the status reads behind the trust-control plane.
chio finding operator serve --profile ./market/operator-profile.jsonFor anything longer-lived than a terminal, the pilot ships a unit that keeps operator state writable in exactly one place and exposes seller repositories read-only:
[Service]
Type=simple
User=chio
Group=chio
UMask=0077
ExecStart=/usr/local/bin/chio finding operator serve --profile /var/lib/chio/cognition-market/operator-profile.json
Restart=on-failure
RestartSec=2
Delegate=memory pids
MemoryMax=8G
MemorySwapMax=0
TasksMax=768
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/srv/chio/cognition-market-repositories
ReadWritePaths=/var/lib/chio/cognition-marketDelegate=memory pids is what gives packaging its cgroup branch; ReadOnlyPaths is what keeps a seller-controlled Git read from reaching operator state. Prepare both roots before starting the service, and stage each seller repository under the read-only root rather than pointing --repository-root at a working checkout.
One writer, enforced by a lease
The single-operator profile allows exactly one concurrent writer, and that is not a tuning parameter. The serving owner holds a lease recorded in a singleton row of the authority database (crates/platform/chio-store-sqlite/src/serving_owner.rs:46-59), and a rollback anchor proves the database has not moved underneath it. A second process pointed at the same profile does not race the first; it refuses to start.
$ chio finding operator serve --profile ./market/operator-profile.jsoncrates/platform/chio-store-sqlite/src/serving_owner.rs:142at fe56570Verify the result
Two commands answer is this venue working, and both are safe to run as often as you like.
operator tick reconciles durable work and prints the health counters. It reads the retained admission bundles, proof bundles and terminals out of the operator database, the durable purchase jobs, and the rail captures behind them, then replays any admission job that did not complete (crates/products/chio-cli/src/cli/dispatch/finding/operator.rs:1124-1162). On a venue that has done no work every counter is zero, which is the answer you want immediately after init.
$ chio finding operator tick --profile ./market/operator-profile.jsonbundles: 0 proofs: 0 terminals: 0 purchase_jobs: 0 captures: 0 reconciled_jobs: 0 failed_admission_jobs: 0
crates/products/chio-cli/src/cli/dispatch/finding/operator.rs:1124-1162at fe56570The second check goes over HTTP and proves the service is answering as the venue rather than as a socket. A topic prefix is required; the index answers with a header row and a count even when it holds nothing.
$ chio finding search --control-url http://127.0.0.1:7143 --topic-prefix coding/FINDING_ID TOPIC EXPIRES_AT ADMISSION count: 0 next_cursor: -
crates/products/chio-cli/src/cli/dispatch/finding.rs:411-431at fe56570To watch a real sale move through these counters, run the pilot's seller and buyer agents against the venue and re-run tick. The captured sequence is on The Cognition Market.
The status epoch and its cadence
A buyer that pins the venue's status feed will not pay for a finding whose current signed root does not include it. Publishing that root is the one piece of venue operation the process does not do for you.
Install the cadence outside the process, with a single-writer lease
docs/release/CHIO_FINDING_MARKET_RUNBOOK.md).The protocol values the feed is bound to are fixed, and a different nonce or backend version is a different protocol that must reject:
| Value | Fixed at |
|---|---|
| Status domain | chio.finding.status.v1 |
| Key-domain nonce | 3318287169837494 |
| Sparse-map depth | 256 |
| Epoch artifact | chio.finding.status-epoch.v1 |
| Portable proof input | chio.finding.status-proof-input.v1 |
Each cadence run reads the durable feed floor and the finalized anchor set, collects the bounded candidate batches, inserts every eligible key in one transactional sparse-map update, advances the map epoch exactly once, signs the complete epoch, and stores its exact canonical bytes before making it current. After the floor advances it re-queries the refresh candidates, because the new epoch can displace proofs that were current when the run started. Failed items stay retryable; a conflicting identity or an ambiguous finality is quarantined rather than rewritten. The full ordered procedure is in docs/release/CHIO_FINDING_MARKET_RUNBOOK.md.
Reads go through two routes, both of which preserve the exact canonical epoch and proof bytes in bounded base64 fields so a caller can verify them locally rather than reconstructing signed bytes from copied JSON:
pub(crate) const FINDING_STATUS_ROOT_PATH: &str = "/v1/findings/status/{feed}/root";
pub(crate) const FINDING_STATUS_PROOF_PATH: &str = "/v1/findings/status/{feed}/proof/{finding_id}";Both fail closed on the feed identity. A venue that has not been given a governance-pinned feed answers the root route with a refusal rather than an empty root:
$ curl -s http://127.0.0.1:7143/v1/findings/status/local-status/root{"error":"urn:chio:error:cli:other [cli:error]: finding-market status feed does not match the configured feed (Preserve the original message and migrate the call site to a specific registry code when touched.)"}crates/platform/chio-control-plane/src/trust_control/finding_status_handlers.rs:629-671at fe56570Operators and buyers verify the same bytes through the CLI. The command requires the governance-pinned feed, the operator authorization, the current service bond, a durable rollback floor and a freshness limit, and it advances that floor atomically on every verified observation:
chio finding status \
--id <finding-id> \
--feed <governance-pinned-feed-id> \
--operator-authorization <governance-pinned-authorization.json> \
--service-bond <governance-pinned-current-service-bond.json> \
--rollback-floor <durable-status-floor.json> \
--max-epoch-age-secs <deployment-freshness-limit>Reuse one rollback-floor path for every query against that feed and stable operator identity, and back up the sibling <rollback-floor>.retractions/ directory with it. The floor is what makes a lower epoch detectable; the tombstones are what keep a retraction sticky.
Enforced ceilings
Every ceiling the market enforces is inventoried in docs/market/CAPACITY.md, and none of it is a model: each row is a constant or a configured value, and reaching one refuses work rather than degrading a guarantee. The rows a single-operator venue meets are these.
The single-operator profile
| Ceiling | Value | Set by |
|---|---|---|
| Concurrent discovery reads | 8 | MAX_READ_COMPANIONS, crates/platform/chio-store-sqlite/src/read_companion.rs:25 |
| Concurrent writers | 1 | the serving-owner lease, crates/platform/chio-store-sqlite/src/serving_owner.rs:46-59 |
| Blocking admission entrants | 1 | the cross-process admission-job lock, verified_fix_admission_lock.rs:7-8 |
| Maximum seller price | 450 USD minor units, so $4.50 | VERIFIED_FIX_MAXIMUM_SALE_EXPOSURE_UNITS, crates/platform/chio-control-plane/src/trust_control/finding_verified_fix.rs:80 |
| Retained seller submission and retraction jobs | 256 combined | MAX_RETAINED_SELLER_JOBS, crates/products/chio-cli/src/cli/dispatch/finding/operator.rs:63 |
Discovery reads lease a read-only companion connection from a bounded pool, so they neither queue behind a write transaction nor behind each other; a ninth concurrent reader waits for a lease to return rather than opening a connection past the bound. Live seller admission and operator tick share the admission-job lock, so only one seller submission or retraction enters blocking execution at a time and overlapping requests receive HTTP 503 with the same durable identity to retry. The lock is a file in the deployment directory with its own deadline:
const ADMISSION_JOB_LOCK_FILE: &str = ".finding-admission-jobs.lock";
const ADMISSION_JOB_LOCK_TIMEOUT: Duration = Duration::from_secs(300);The price cap is expressed in minor units of the profile's currency, which initialization sets to USD. The pilot's seller agent defaults to 300 units, which is $3.00, and a submission priced at zero or above the cap is rejected with price_units exceeds the verified-fix sale exposure (crates/platform/chio-control-plane/src/trust_control/finding_operator_seller_routes.rs:113-115).
What the operator database retains
Retention is bounded per store, both by row count and by bytes. At capacity new work fails closed while exact retries stay replayable, so a full store never silently drops the record a retry depends on.
const MAX_RETAINED_CHALLENGE_POLICIES: i64 = 100_000;
const MAX_RETAINED_AUDIT_ROUNDS: i64 = 10_000;
const MAX_RETAINED_AUDIT_ROUND_BYTES: usize = 16 * 1024 * 1024;
const MAX_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
const MAX_PROOF_BYTES: usize = 24 * 1024 * 1024;
const MAX_PURCHASE_JOB_BYTES: usize = 2 * 1024 * 1024;
const MAX_RETAINED_BUNDLES: i64 = 10_000;
const MAX_RETAINED_PURCHASE_JOBS: i64 = 10_000;
const MAX_RETAINED_TERMINAL_BYTES: i64 = 256 * 1024 * 1024;
const MAX_RETAINED_SELLER_ARTIFACT_CLAIMS: i64 = 256;
const MAX_SELLER_ARTIFACT_BYTES: i64 = (8 + 4 + 24) * 1024 * 1024 + 256 * 1024;MAX_RETAINED_PURCHASE_JOBS is the one an operator notices first: at ten thousand durable purchase jobs, new purchases are refused and exact retries of existing ones still replay. MAX_RETAINED_TERMINAL_BYTES bounds retained purchase-result bodies at 256 MiB with the same behavior.
What a seller's replay is allowed
Every test the seller names runs inside a private size-capped tmpfs with no network, a cleared environment, bounded output, and hard memory, CPU, process, descriptor and file-size limits. Memory, swap and process limits are enforced across the complete descendant tree by cgroup v2, with process-local rlimits kept as defense in depth.
const MAX_COMMAND_OUTPUT_BYTES: usize = 4 * 1024 * 1024;
const TEST_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
pub(super) const PACKAGE_WORK_TIMEOUT: Duration = Duration::from_secs(300);
const TEST_SANDBOX_ADDRESS_SPACE_BYTES: u64 = 6 * 1024 * 1024 * 1024;
const TEST_SANDBOX_FILE_BYTES: u64 = 1024 * 1024 * 1024;
const TEST_SANDBOX_TMPFS_BYTES: u64 = 4 * 1024 * 1024 * 1024;
const TEST_SANDBOX_PROCESS_LIMIT: u64 = 512;
const TEST_SANDBOX_OPEN_FILE_LIMIT: u64 = 1024;
const TEST_SANDBOX_CPU_SECS: u64 = 300;All seller-controlled Git reads, staging, baseline and candidate commands, and patch generation share one five-minute aggregate package deadline. Repository clone and checkout staging has its own five-minute deadline, a 1 GiB aggregate storage ceiling, a 75,000-entry ceiling and a per-file size limit, and the operator reserves the full transient staging budget plus publication headroom inside an 8 GiB and 100,000-entry package and report storage ceiling (examples/cognition-market-pilot/README.md).
What changes on the hosted profile
A deployment that needs more than one concurrent writer wants the hosted PostgreSQL profile rather than a larger bound here. Its ceilings are per tenant and mostly configured rather than constant:
| Ceiling | Value | Set by |
|---|---|---|
| Live proof nonces | 1_000..=10_000_000, one value for every tenant | identity.nonceCapacity, crates/platform/chio-control-plane/src/trust_control/finding_hosted_profile.rs:135 bounded at :747 |
| Retained request admissions | 64 per live-proof slot | RETAINED_BINDINGS_PER_NONCE_SLOT, crates/platform/chio-finding-market-store-postgres/src/auth.rs:25 |
| Expiry sweep cadence | 3600 seconds, or on capacity pressure | DPOP_SWEEP_INTERVAL_SECS, crates/platform/chio-finding-market-store-postgres/src/auth.rs:17 |
| Concurrent jobs, queued jobs, monthly spend units | per tenant, 1..=1_024 concurrent | HostedTenantLimits, crates/platform/chio-finding-market-store-postgres/src/tenant.rs:11-31 |
| Retained jobs in every state | per tenant, 1..=10_000_000 | database.maxJobsPerTenant, crates/platform/chio-finding-market-store-postgres/src/lib.rs:143-144 |
| In-flight requests per replica | 1_024 | MAX_CONCURRENT_REQUESTS, crates/products/chio-finding-market-server/src/main.rs:31 |
| Request body | 4 MiB | MAX_HTTP_BODY_BYTES, crates/products/chio-finding-market-server/src/main.rs:32 |
| Readiness answer lifetime | 1 second | READINESS_ANSWER_LIFETIME, crates/platform/chio-finding-hosted-edge/src/server.rs:533 |
The monthly spend ceiling is enforced inside the same statement that inserts a reservation, by a trigger the runtime role cannot bypass: the runtime holds no write privilege on the accumulator, and a charged reservation's units are immutable once written. When a ceiling binds too early, the fix is per row and docs/market/CAPACITY.md names it. Raising a constant is the wrong move: each one bounds a resource shared by every caller, and raising it moves the failure from a refused request to an exhausted pool, a full disk, or an unbounded queue.
Failures and recovery
Initialization does not match the existing deployment
A re-run with a changed listen address, principal or payout destination is refused rather than applied. The deployment keeps the identity it already published.
$ chio finding operator init --directory ./market --repository-root ./repos \
--listen 127.0.0.1:7999examples/cognition-market-pilot/README.mdat fe56570HTTP 503 from a busy lane
Purchase execution uses one non-queued blocking lane, and public proof reads use a separate one-response lane with 64 KiB streaming chunks and a 30-second absolute egress deadline. A busy lane returns HTTP 503 instead of accumulating work, and the caller retries the same durable identity. Overlapping seller submissions get the same answer from the admission lock. A 503 here is back-pressure, not a fault; treat a sustained one as a sizing signal.
An expired purchase ask
A prepared purchase ask is checked against current operator time before its first reservation. An expired ask cannot reserve funds and the buyer must prepare a fresh purchase request. An open reservation that expires during a restart is durably expired and returns the same rejection on every retry, so a retry loop converges instead of oscillating.
A finding stuck in publication_pending
Inspect the stages in order rather than reaching for the store. Confirm the liability is finalizing and not appealed, reversed or merely evaluated; confirm the seller-impair and enforcement-root intents are the exact identities the signed enforcement artifact bound; confirm the impairment receipt is final and unambiguous under the pinned chain-finality policy; confirm the retraction intent is still the original durable item; and confirm the authorization, service bond, signing key, feed floor and sparse-map store are all available. Then retry the same item. Never mint a replacement intent to bypass a conflict, and keep purchases denied for the whole investigation.
Two roots at one map epoch
Any second epoch id or root for an already observed map epoch is equivocation. Freeze status publication and status-gated purchases for the feed, preserve both exact signed byte sequences with their authorization snapshots and observation times, open the objective service-bond penalty path, and audit every buyer and kernel floor for the conflicting epoch. Resume with a strictly greater map epoch and the same feed id, fixed nonce and complete retracted-key set. Never choose one conflicting root by local timestamp and never reset the epoch to zero.
Restart and restore
On restart the service loads and verifies the durable feed floor, the exact current signed epoch, the sparse leaves, the sticky status rows and the outbox before serving proofs or allowing purchases. If any required state is missing or inconsistent it keeps the feed unavailable and denies status-gated reads and purchases. An established feed that starts without a floor is not an empty feed; it is a fail-closed recovery incident. Restore only from a backup that contains at least the last externally observed map epoch: a backup with a lower floor is unsafe to serve even while its epoch is still inside its validity window.
Packaging refuses on a workstation
Three host conditions make seller packaging fail for reasons the seller cannot act on, all of them recorded against the source rather than this page. A version-manager shim on python3 (mise, pyenv and asdf all install one) resolves to the manager instead of an interpreter and admission returns failed to inspect Python standard library. A runtime tree past 20,000 visited entries aborts with sandbox runtime tree exceeded its entry bound, which a stock interpreter install under a version manager can reach. And the sandbox's optional tool list does not admit awk, so a test command that uses it fails with the seller's own exit code and reads as a broken fix. Point the operator at a system interpreter, keep the test command inside the admitted tool set, and the flow completes.
What to alert on
The runbook names the full set. The ones that matter most for a single-operator venue: epoch publication misses its cadence; an established feed starts without a floor; a lower-epoch replay or a same-epoch equivocation; an authorization, key epoch, rotation or revocation mismatch; a service bond missing, expired, revoked or below its allocation; a pending intent past its inclusion SLA; and an outbox retry count or age past the operator threshold. Every one of them keeps the affected qualified operation fail-closed until the exact durable evidence is reconciled.
Next Steps
- The Cognition Market · the artifacts, the six flows, and one captured sale
- Hosted Cognition Market · the tenant-isolated profile, hosted validation, and canary evaluation
- The Finding Record · every field of the artifact a seller signs
- Settlement · the rails a captured purchase settles over