Chio/Docs
LOGIN · JOIN

Build an agent cooperative

Let independent operators authorize shared computation, including a registered 2-of-3 threshold settlement with retained local-credit accounting.

Guide chio@5f63e6e

On this page

A cooperative lets an effect happen only when its members agree. This application gives independent members their own policies and asks them to authorize one statistical computation; the bilateral path gathers two signatures over iroh, and the threshold path creates a 2-of-3 FROST group that executes a registered settlement, transferring local work credits and computing the result in one retained transaction.

Run the cooperative

Two of three members authorize one computation and its settlement

Build it. Run it. Inspect it.

Two operators authorize shared research compute

Independent member policies decide an exact computation. Both signatures travel over Iroh before the compute owner accepts the work.

Download project
Your inputs
Separate numbers with commas. The result uses these exact samples.

Hosted records are retained for seven days. Use the downloaded project for local work and your own model credentials.

Loading the verified example result…

Change the numeric samples or the member budget. In bilateral mode, either required operator can refuse. In threshold mode, three member processes keep separate signing custody and two eligible members can authorize the registered settlement. Inspect the computation and balances, then the changed-parameter and repeat-spend refusals. Credits are local to this application.

Members sign and the compute owner verifies and executes

Each member’s policy sits beside its key share. The compute owner accepts two of three signatures over the exact action, then debits and computes in one transaction.

Agent cooperativeChoose a lens.
proposaldebits oncesignssignschanged parameterspProposerσ₁Member 1own policyσ₂Member 2own policyσ₃Member 3own key shareKCompute owneraccepts 2 of 3BLocal creditsρReceiptMembers: own policy, own key sharechanged parameterspProposerσ₁σ₂σ₃KCompute ownerBCreditsρReceipt

Two of three members authorize one action

Each member keeps its policy beside its signing key, and the threshold ceremony gives each member only the private packages addressed to it. The compute owner verifies the registered action against the current roster and epoch; a signature over changed samples or parameters is refused before the ledger. FROST threshold signatures.

The cooperative application's threshold path: the proposer, three members with separate key shares, and the compute owner's ledger. The bilateral path over iroh and the key-generation rounds are left out.

Each member’s policy sits beside its signing key

The bilateral receiver evaluates the exact proposed work before it returns a signature. Its policy limits participation, parameters, lifetime, and local resource allowance. The proposer can request agreement and never replaces a member’s decision, and iroh carries the request between independent processes.

personal-network/src/joint.rsL69–118rust
pub fn allow(
    policy: &Policy,
    action: &Action,
    local: &str,
    other: &str,
    gate: &DirectoryGate,
) -> Result<()> {
    anyhow::ensure!(
        policy.accept,
        "This member declined the proposal under its local policy"
    );
    anyhow::ensure!(
        action.schema == "chio.cooperative.computation.v1",
        "Unsupported joint action"
    );
    uuid::Uuid::parse_str(&action.nonce)?;
    let now = now_ms();
    anyhow::ensure!(
        action.issued_at_ms <= now
            && now < action.expires_at_ms
            && action.expires_at_ms - action.issued_at_ms <= 300_000,
        "Proposal expired or has an excessive validity window"
    );
    anyhow::ensure!(
        action.participants.len() == 2
            && action.participants.contains(&local.to_string())
            && action.participants.contains(&other.to_string())
            && local != other,
        "Proposal does not bind these two independent members"
    );
    anyhow::ensure!(
        action.resource_id == "shared-research-compute"
            && !action.values.is_empty()
            && action.values.len() <= policy.max_values
            && action.values.iter().all(|v| v.is_finite())
            && action.work_units == action.values.len() as u64
            && action.work_units <= policy.max_work_units,
        "Computation exceeds this member's resource policy"
    );
    let directory = gate.directory();
    anyhow::ensure!(
        directory.expires_at_unix_ms() > now
            && action
                .participants
                .iter()
                .all(|id| directory.resolve_passport_key(id).is_some()),
        "A proposal member is no longer admitted"
    );
    Ok(())
}

Open this excerpt in the complete source

In the verified run, events 02 and 03 record each member’s independent execution with its original receipts.

Threshold custody is created at each member

The threshold ceremony uses distributed key generation, and each member receives only the private round-two packages addressed to it. The coordinator transports public receipts, never the full set of private packages. Completion checks the public transcript and the member’s own packages before it retains the key share.

cooperative/src/threshold/member.rsL202–236rust
async fn complete(
    State(node): State<Arc<Node>>,
    h: HeaderMap,
    Json(receipts): Json<Vec<FrostDkgPackageReceipt>>,
) -> Api {
    admin(&node, &h).map_err(error)?;
    let mut guard = node.ceremony.lock().map_err(error)?;
    let state = guard.as_mut().ok_or_else(|| error("No ceremony"))?;
    let complete = complete_frost_ceremony_scoped(
        &state.config,
        state
            .secret
            .take()
            .ok_or_else(|| error("Round two already consumed"))?,
        &state.round1,
        &state.inbound,
        &receipts,
    )
    .map_err(error)?;
    private_bytes(
        &node.config.root.join("key-package.custody"),
        complete.key_package.custody_bytes(),
    )
    .map_err(error)?;
    let value = json!({
        "group_public_key":complete.group_public_key,
        "verification_shares":complete.verification_shares,
        "transcript_digest":complete.transcript_digest,
        "received_private_packages":state.inbound.len()
    });
    state.key = Some(complete.key_package);
    state.public = Some(value.clone());
    state.inbound.clear();
    Ok(Json(value))
}

Open this excerpt in the complete source

The effect owner verifies the registered action

The threshold action binds the exact samples, units, payer, payee, operation ID, and execution fence. The compute owner verifies the registered settlement against the current roster, epoch, completed signing slot, and selected trust keys before it touches its ledger, so a valid signature over other bytes authorizes nothing here.

cooperative/src/threshold/authority.rsL43–129rust
    fn execute(&self, input: Value) -> Result<Value> {
        let settlement: Settlement = serde_json::from_value(input["settlement"].clone())?;
        settlement.validate()?;
        let proof: FrostAuthorizationV1 = serde_json::from_value(input["proof"].clone())?;
        let action = action(&settlement)?;
        let digest = action.action_digest()?;
        let registration = frost_action_registration(FrostAuthorizationDomain::SettleCommitment)
            .context("Settlement is not registered")?;
        let contract = registration.ladder_contract_digest()?;
        let expected = ExpectedFrostAuthorization {
            domain: FrostAuthorizationDomain::SettleCommitment,
            ladder_action_class: registration.ladder_action_class,
            ladder_contract_digest: &contract,
            scope_id: super::SCOPE,
            resource_id: &settlement.operation_id,
            resource_version: 1,
            resource_fence: 1,
            action_digest: &digest,
        };
        verify_for_execution(
            &proof,
            &expected,
            &self.0.active()?,
            &*self.0,
            &*self.0,
            &self.0.trust()?,
            now(),
        )?;
        // The local credit transfer and its useful deterministic computation commit together.
        let mut db = self
            .0
            .db
            .lock()
            .map_err(|_| anyhow::anyhow!("Settlement store lock failed"))?;
        let tx = db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
        let count: i64 = tx.query_row(
            "SELECT COUNT(*) FROM settlements WHERE id=?1",
            [&settlement.operation_id],
            |r| r.get(0),
        )?;
        anyhow::ensure!(
            count == 0,
            "This operation already settled; inspect the retained result instead of spending again"
        );
        let raw: String = tx.query_row("SELECT body FROM records WHERE id='credits'", [], |r| {
            r.get(0)
        })?;
        let mut credits: Value = serde_json::from_str(&raw)?;
        let available = credits["research"]
            .as_u64()
            .context("Invalid payer balance")?;
        anyhow::ensure!(
            available >= settlement.units,
            "The research account has insufficient local credits"
        );
        let mean = settlement.values.iter().sum::<f64>() / (settlement.values.len() as f64);
        let output = json!({
            "mean":mean,
            "samples":settlement.values.len(),
            "operation_id":settlement.operation_id,
            "local_credits_transferred":settlement.units,
            "action_digest":digest,
            "authorization_id":proof.body.authorization_id
        });
        credits["research"] = json!(available - settlement.units);
        credits["compute"] = json!(
            credits["compute"]
                .as_u64()
                .context("Invalid recipient balance")?
                + settlement.units
        );
        tx.execute(
            "UPDATE records SET body=?1 WHERE id='credits'",
            [serde_json::to_string(&credits)?],
        )?;
        tx.execute(
            "INSERT INTO settlements(id,action_digest,result) VALUES(?1,?2,?3)",
            rusqlite::params![
                settlement.operation_id,
                digest,
                serde_json::to_string(&output)?
            ],
        )?;
        tx.commit()?;
        Ok(output)
    }
}

Open this excerpt in the complete source

Event 04 completes the jointly authorized computation, a mean of 24.0 over four samples, with receipt c812a54b…31af6a.

Authority and accounting are retained together

The compute owner retains its current anchors and the operation ledger. The test kills and restarts that process and resubmits the approved action, and the unique operation debits the account once. The example has one compute operator with distinct authority keys, whose database and keys form one backup unit.

cooperative/src/threshold/authority.rsL162–183rust
async fn execute(State(node): State<Arc<Node>>, h: HeaderMap, Json(input): Json<Value>) -> Api {
    admin(&node, &h).map_err(error)?;
    let result = node
        .host
        .call(
            &node.run,
            "research-operator",
            &node.cap,
            "cooperative",
            "settle_and_compute",
            input,
        )
        .await
        .map_err(error)?;
    Ok(Json(json!({
        "allowed":result.allowed,
        "output":result.output,
        "reason":result.reason,
        "receipt_id":result.receipt_id,
        "run":node.run.snapshot().map_err(error)?
    })))
}

Open this excerpt in the complete source

Too few approvals, changed parameters, and a replayed settlement move no credits

The verified run is the bilateral path: one proposal, two independent members, one computation.

Terminalbash
./run --run input.json
Retained events, captures/verified.json
01  cooperative.proposed      research-lab
    Request a bounded research computation
02  cooperative.member        member-host
    Independent member execution and original receipts
03  cooperative.member        member-host
    Independent member execution and original receipts
04  cooperative.completed     compute-owner
    Jointly authorized computation completed
    receipt c812a54b…31af6a
  • Collect too few approvals: no computation or credit transfer occurs.
  • Change samples or settlement parameters after signing: verification refuses the action.
  • Submit a proof without the selected current anchors: the effect owner refuses it.
  • Restart and replay a completed settlement: balances and the effect count stay unchanged.

The downloadable tests assert each effect or its absence: run cargo test --locked -p chio-cooperative from the extracted project.

Run the cooperative on your machine

Keep the run directory to inspect the joint authorization, the computation, and the local-credit settlement.

From the extracted projectbash
cd chio-cooperative
./run

The archive holds the application, the shared host code, tests, sample input, pinned dependencies, and a README. The launcher builds and runs the Rust source included in this archive. Install Rust 1.94.1 and the README’s native prerequisites; filesystem isolation and operator workloads require the listed Linux host profile. Cargo resolves immutable public dependencies, so a sibling checkout is unnecessary.

For model workers, set OPENROUTER_API_KEY on the local host and select model mode; credentials stay on that host and never enter the docs form. The qualified model is openai/gpt-4.1-mini. Analysis, retrieval, and deterministic worker modes run without a model key. Ctrl-C stops the local web process and its active child work and keeps the run directory.

Download identity and source revision

Download complete project · View public source

Project archive SHA-256text
1826c9c7de1944e6c4496527af5c2781fdfc573b1414bec22b74c48ff717d042

Register another action the owner can check atomically

Define another registered action whose effect its owner can check atomically. Keep member policy local, bind every effectful parameter into the signed action, and retain the operation fence with the effect. An externally settled asset plugs in as a settlement adapter, an optional module beside the local work credits.

Next: build a research swarm