# Arkhai > Open infrastructure for agent-driven markets: escrow, compute, retrieval, code bounties, and natural-language agreements. ## Main Pages - [Homepage](https://www.arkhai.io/) - [Arkhai Compute](https://compute.arkhai.io/) — institutional home for deliverable compute-market infrastructure, beginning with Simple Compute Market - [Blog](https://www.arkhai.io/blog) - [Documentation](https://www.arkhai.io/docs) - [Alkahest Documentation](https://www.arkhai.io/docs/alkahest) - [Modular Markets Documentation](https://www.arkhai.io/docs/modular-markets) - [Whitepaper](https://www.arkhai.io/whitepaper) - [Contact](https://www.arkhai.io/contact) - [Press Kit](https://www.arkhai.io/press-kit) - [Privacy Policy](https://www.arkhai.io/privacy-policy) ## Products - [Alkahest](https://www.arkhai.io/products/alkahest) - [Arkhai Compute](https://compute.arkhai.io/) — begins with the Simple Compute Market software - [Agentic RAG](https://www.arkhai.io/products/agentic-rag) - [Git Commit Marketplace](https://www.arkhai.io/products/git-commit-marketplace) - [Active Bounties](https://www.arkhai.io/products/git-commit-marketplace/active-bounties) - [Natural Language Agreements](https://www.arkhai.io/products/natural-language-agreements) ## Documentation Sections - [Alkahest Docs](https://www.arkhai.io/docs/alkahest) - [Modular Markets Docs](https://www.arkhai.io/docs/modular-markets) — Compositional Game Theory (CGT) pattern language ## Historical content Dated posts and the whitepaper preserve their publication-time context. They do not override current product, actor, availability, license, or commercial boundaries. ## Historical revenue-model context The July 2024 whitepaper records publication-time product, token, fee, revenue, service, roadmap, and availability proposals. It is not a statement of current capabilities, offerings, contracts, revenue, or deployment status. Current Arkhai Compute and SCM boundaries above take precedence. ## pt 3 - Composing Demands import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Escrow Flow (pt 3 - Composing Demands) The logical arbiter contracts AnyArbiter and AllArbiter can be used to compose demands. For example, you could demand that - a task can only be completed by a particular account, and is validated by a trusted third party - a task is completed before a particular deadline, and validated by any of a list of trusted third parties - uptime for a server provision is above a certain threshold, and is actually the expected service ## Using logical arbiters You can use AllArbiter to demand multiple conditions at the same time. For example, that your task is completed by a particular individual before a deadline, and validated by a third party. ```solidity // Example: Require job completed by specific recipient AND validated by oracle bytes memory recipientDemand = abi.encode( RecipientArbiter.DemandData({ recipient: bob // Only Bob can fulfill this }) ); bytes memory oracleDemand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: charlie, // Charlie validates the result data: abi.encode("complete the analysis task") }) ); // Compose with AllArbiter - both conditions must be met bytes memory composedDemand = abi.encode( AllArbiter.DemandData({ arbiters: [address(recipientArbiter), address(trustedOracleArbiter)], demands: [recipientDemand, oracleDemand] }) ); // Deposit escrow with the composed demand IERC20(erc20Token).approve(address(erc20EscrowObligation), 100 * 10**18); bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: erc20Token, amount: 100 * 10**18, arbiter: address(allArbiter), demand: composedDemand }), block.timestamp + 86400 ); ``` ```typescript // Example: Require job completed by specific recipient AND validated by oracle const recipientDemand = encodeAbiParameters( parseAbiParameters("(address recipient)"), [{ recipient: bob }], ); const oracleDemand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: charlie, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "complete the analysis task" }, ]), }); // Compose with AllArbiter - both conditions must be met const composedDemand = aliceClient.arbiters.encodeAllArbiterDemand({ arbiters: [recipientArbiter, trustedOracleArbiter], demands: [recipientDemand, oracleDemand], }); // Deposit escrow with the composed demand (using permit for gasless approval) const { attested: escrow } = await aliceClient.erc20.escrow.nonTierable.permitAndCreate( { address: erc20Token, value: parseEther("100") }, { arbiter: allArbiter, demand: composedDemand }, BigInt(Math.floor(Date.now() / 1000) + 86400), ); ``` ```rust use alloy::sol_types::SolValue; use alkahest_rs::clients::arbiters::MultiArbiter; // Example: Require job completed by specific recipient AND validated by oracle let recipient_demand_data = contracts::RecipientArbiter::DemandData { recipient: bob_address, }; let recipient_demand = recipient_demand_data.abi_encode().into(); let oracle_demand_data = contracts::TrustedOracleArbiter::DemandData { oracle: charlie_address, data: "complete the analysis task".abi_encode().into(), }; let oracle_demand = oracle_demand_data.abi_encode().into(); // Compose with AllArbiter - both conditions must be met let composed_demand_data = MultiArbiter::DemandData { arbiters: vec![recipient_arbiter_address, trusted_oracle_arbiter_address], demands: vec![recipient_demand, oracle_demand], }; let composed_demand = alice_client .arbiters() .encode_multi_arbiter_demand(&composed_demand_data); // Deposit escrow with the composed demand (using permit for gasless approval) let price = Erc20Data { address: erc20_token, value: U256::from(100e18), }; let item = ArbiterData { arbiter: all_arbiter_address, demand: composed_demand, }; let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .permit_and_create(&price, &item, expiration_time) .await?; ``` ```python from eth_abi import encode from alkahest_py import AllArbiterDemandData, RecipientArbiterDemandData, TrustedOracleArbiterDemandData # Example: Require job completed by specific recipient AND validated by oracle recipient_demand = RecipientArbiterDemandData(bob).encode_self() oracle_inner_data = encode(['string'], ['complete the analysis task']) oracle_demand = TrustedOracleArbiterDemandData(charlie, oracle_inner_data).encode_self() # Compose with AllArbiter - both conditions must be met composed_demand = AllArbiterDemandData( [recipient_arbiter, trusted_oracle_arbiter], [recipient_demand, oracle_demand] ).encode_self() # Deposit escrow with the composed demand (using permit for gasless approval) price = {"address": erc20_token, "value": 100} item = {"arbiter": all_arbiter, "demand": composed_demand} escrow_receipt = await alice_client.erc20.escrow.non_tierable.permit_and_create( price, item, int(time.time()) + 86400 ) ``` ```bash # Encode individual demands alkahest arbiter encode-demand --type recipient \ --recipient 0xBOB # → { "encoded": "0xRECIPIENT_DEMAND" } alkahest arbiter encode-demand --type trusted-oracle \ --oracle 0xCHARLIE --data 0x # → { "encoded": "0xORACLE_DEMAND" } # Compose with AllArbiter - both conditions must be met # Use addresses from: alkahest config show --chain base-sepolia alkahest arbiter encode-demand --type all \ --demands '[{"arbiter":"0xRECIPIENT_ARBITER","demand":"0xRECIPIENT_DEMAND"},{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE_DEMAND"}]' # → { "encoded": "0xCOMPOSED_DEMAND" } # Create escrow with the composed demand alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xERC20_TOKEN --amount 100000000000000000000 \ --arbiter 0xALL_ARBITER \ --demand 0xCOMPOSED_DEMAND \ --expiration 1735689600 \ --approve ``` You can use AnyArbiter when multiple alternative conditions can be considered valid. For example, if you accept a decision from any of a list of trusted third party oracles, or if there are different proof mechanisms that equally ensure the validity of a result. ```solidity // Example: Accept validation from ANY of multiple trusted oracles bytes memory oracle1Demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: charlie, data: abi.encode("verify computation result") }) ); bytes memory oracle2Demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: dave, data: abi.encode("verify computation result") }) ); bytes memory oracle3Demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: eve, data: abi.encode("verify computation result") }) ); // Compose with AnyArbiter - any one oracle can validate bytes memory composedDemand = abi.encode( AnyArbiter.DemandData({ arbiters: [ address(trustedOracleArbiter), address(trustedOracleArbiter), address(trustedOracleArbiter) ], demands: [oracle1Demand, oracle2Demand, oracle3Demand] }) ); // Create escrow with flexible oracle validation bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: erc20Token, amount: 50 * 10**18, arbiter: address(anyArbiter), demand: composedDemand }), block.timestamp + 7200 // 2 hour expiration ); ``` ```typescript // Example: Accept validation from ANY of multiple trusted oracles const oracle1Demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: charlie, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "verify computation result" }, ]), }); const oracle2Demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: dave, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "verify computation result" }, ]), }); const oracle3Demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: eve, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "verify computation result" }, ]), }); // Compose with AnyArbiter - any one oracle can validate const composedDemand = aliceClient.arbiters.encodeAnyArbiterDemand({ arbiters: [trustedOracleArbiter, trustedOracleArbiter, trustedOracleArbiter], demands: [oracle1Demand, oracle2Demand, oracle3Demand], }); // Create escrow with flexible oracle validation (using approveAndCreate) const { attested: escrow } = await aliceClient.erc20.escrow.nonTierable.approveAndCreate( { address: erc20Token, value: parseEther("50") }, { arbiter: anyArbiter, demand: composedDemand }, BigInt(Math.floor(Date.now() / 1000) + 7200), // 2 hour expiration ); ``` ```rust // Example: Accept validation from ANY of multiple trusted oracles let query = "verify computation result".abi_encode(); let oracle1_demand = contracts::TrustedOracleArbiter::DemandData { oracle: charlie_address, data: query.clone().into(), }.abi_encode().into(); let oracle2_demand = contracts::TrustedOracleArbiter::DemandData { oracle: dave_address, data: query.clone().into(), }.abi_encode().into(); let oracle3_demand = contracts::TrustedOracleArbiter::DemandData { oracle: eve_address, data: query.into(), }.abi_encode().into(); // Compose with AnyArbiter - any one oracle can validate let composed_demand_data = MultiArbiter::DemandData { arbiters: vec![ trusted_oracle_arbiter_address, trusted_oracle_arbiter_address, trusted_oracle_arbiter_address, ], demands: vec![oracle1_demand, oracle2_demand, oracle3_demand], }; let composed_demand = alice_client .arbiters() .encode_multi_arbiter_demand(&composed_demand_data); // Create escrow with flexible oracle validation (using approve_and_create) let price = Erc20Data { address: erc20_token, value: U256::from(50e18), }; let item = ArbiterData { arbiter: any_arbiter_address, demand: composed_demand, }; let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .approve_and_create(&price, &item, current_timestamp + 7200) // 2 hour expiration .await?; ``` ```python # Example: Accept validation from ANY of multiple trusted oracles query_data = encode(['string'], ['verify computation result']) oracle1_demand = TrustedOracleArbiterDemandData(charlie, query_data).encode_self() oracle2_demand = TrustedOracleArbiterDemandData(dave, query_data).encode_self() oracle3_demand = TrustedOracleArbiterDemandData(eve, query_data).encode_self() # Compose with AnyArbiter - any one oracle can validate composed_demand = AnyArbiterDemandData( [trusted_oracle_arbiter, trusted_oracle_arbiter, trusted_oracle_arbiter], [oracle1_demand, oracle2_demand, oracle3_demand] ).encode_self() # Create escrow with flexible oracle validation (using approve_and_create) price = {"address": erc20_token, "value": 50} item = {"arbiter": any_arbiter, "demand": composed_demand} escrow_receipt = await alice_client.erc20.escrow.non_tierable.approve_and_create( price, item, int(time.time()) + 7200 # 2 hour expiration ) ``` ```bash # Encode individual oracle demands alkahest arbiter encode-demand --type trusted-oracle \ --oracle 0xCHARLIE --data 0x # → { "encoded": "0xORACLE1_DEMAND" } alkahest arbiter encode-demand --type trusted-oracle \ --oracle 0xDAVE --data 0x # → { "encoded": "0xORACLE2_DEMAND" } alkahest arbiter encode-demand --type trusted-oracle \ --oracle 0xEVE --data 0x # → { "encoded": "0xORACLE3_DEMAND" } # Compose with AnyArbiter - any one oracle can validate alkahest arbiter encode-demand --type any \ --demands '[{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE1_DEMAND"},{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE2_DEMAND"},{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE3_DEMAND"}]' # → { "encoded": "0xCOMPOSED_DEMAND" } # Create escrow with flexible oracle validation alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xERC20_TOKEN --amount 50000000000000000000 \ --arbiter 0xANY_ARBITER \ --demand 0xCOMPOSED_DEMAND \ --expiration 1735689600 \ --approve ``` Logical arbiters can be stacked. For example, you could demand that a job is completed before a deadline by a particular party, and validated by any of a list of trusted oracles. ```solidity // Example: Complex composition - deadline AND recipient AND (oracle1 OR oracle2) bytes memory deadlineDemand = abi.encode( TimeBeforeArbiter.DemandData({ time: uint64(block.timestamp + 3600) // Must complete within 1 hour }) ); bytes memory recipientDemand = abi.encode( RecipientArbiter.DemandData({ recipient: bob // Must be fulfilled by Bob }) ); // Create the OR condition for oracles bytes memory oracle1Demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: charlie, data: abi.encode("validate result") }) ); bytes memory oracle2Demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: dave, data: abi.encode("validate result") }) ); bytes memory oracleOrDemand = abi.encode( AnyArbiter.DemandData({ arbiters: [address(trustedOracleArbiter), address(trustedOracleArbiter)], demands: [oracle1Demand, oracle2Demand] }) ); // Combine all conditions with AllArbiter bytes memory finalDemand = abi.encode( AllArbiter.DemandData({ arbiters: [ address(timeBeforeArbiter), address(recipientArbiter), address(anyArbiter) ], demands: [ deadlineDemand, recipientDemand, oracleOrDemand ] }) ); // Create the escrow with nested logical arbiters bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: erc20Token, amount: 200 * 10**18, arbiter: address(allArbiter), demand: finalDemand }), block.timestamp + 7200 // 2 hour overall expiration ); ``` ```typescript // Example: Complex composition - deadline AND recipient AND (oracle1 OR oracle2) const deadlineDemand = encodeAbiParameters( parseAbiParameters("(uint64 time)"), [{ time: BigInt(Math.floor(Date.now() / 1000) + 3600) }], // 1 hour deadline ); const recipientDemand = encodeAbiParameters( parseAbiParameters("(address recipient)"), [{ recipient: bob }], ); // Create the OR condition for oracles const oracle1Demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: charlie, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "validate result" }, ]), }); const oracle2Demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: dave, data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "validate result" }, ]), }); const oracleOrDemand = aliceClient.arbiters.encodeAnyArbiterDemand({ arbiters: [trustedOracleArbiter, trustedOracleArbiter], demands: [oracle1Demand, oracle2Demand], }); // Combine all conditions with AllArbiter const finalDemand = aliceClient.arbiters.encodeAllArbiterDemand({ arbiters: [timeBeforeArbiter, recipientArbiter, anyArbiter], demands: [deadlineDemand, recipientDemand, oracleOrDemand], }); // Create the escrow with nested logical arbiters (using approveAndCreate) const { attested: escrow } = await aliceClient.erc20.escrow.nonTierable.approveAndCreate( { address: erc20Token, value: parseEther("200") }, { arbiter: allArbiter, demand: finalDemand }, BigInt(Math.floor(Date.now() / 1000) + 7200), // 2 hour overall expiration ); ``` ```rust // Example: Complex composition - deadline AND recipient AND (oracle1 OR oracle2) let deadline_demand_data = contracts::TimeBeforeArbiter::DemandData { time: current_timestamp + 3600, // 1 hour deadline }; let deadline_demand = deadline_demand_data.abi_encode().into(); let recipient_demand_data = contracts::RecipientArbiter::DemandData { recipient: bob_address, }; let recipient_demand = recipient_demand_data.abi_encode().into(); // Create the OR condition for oracles let validate_query = "validate result".abi_encode(); let oracle1_demand = contracts::TrustedOracleArbiter::DemandData { oracle: charlie_address, data: validate_query.clone().into(), }.abi_encode().into(); let oracle2_demand = contracts::TrustedOracleArbiter::DemandData { oracle: dave_address, data: validate_query.into(), }.abi_encode().into(); let oracle_or_demand_data = MultiArbiter::DemandData { arbiters: vec![trusted_oracle_arbiter_address, trusted_oracle_arbiter_address], demands: vec![oracle1_demand, oracle2_demand], }; let oracle_or_demand = alice_client .arbiters() .encode_multi_arbiter_demand(&oracle_or_demand_data); // Combine all conditions with AllArbiter let final_demand_data = MultiArbiter::DemandData { arbiters: vec![ time_before_arbiter_address, recipient_arbiter_address, any_arbiter_address, ], demands: vec![deadline_demand, recipient_demand, oracle_or_demand], }; let final_demand = alice_client .arbiters() .encode_multi_arbiter_demand(&final_demand_data); // Create the escrow with nested logical arbiters (using approve_and_create) let price = Erc20Data { address: erc20_token, value: U256::from(200e18), }; let item = ArbiterData { arbiter: all_arbiter_address, demand: final_demand, }; let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .approve_and_create(&price, &item, current_timestamp + 7200) // 2 hour overall expiration .await?; ``` ```python # Example: Complex composition - deadline AND recipient AND (oracle1 OR oracle2) from alkahest_py import TimeBeforeArbiterDemandData, RecipientArbiterDemandData deadline_demand = TimeBeforeArbiterDemandData(int(time.time()) + 3600).encode_self() recipient_demand = RecipientArbiterDemandData(bob).encode_self() # Create the OR condition for oracles validate_query = encode(['string'], ['validate result']) oracle1_demand = TrustedOracleArbiterDemandData(charlie, validate_query).encode_self() oracle2_demand = TrustedOracleArbiterDemandData(dave, validate_query).encode_self() oracle_or_demand = AnyArbiterDemandData( [trusted_oracle_arbiter, trusted_oracle_arbiter], [oracle1_demand, oracle2_demand] ).encode_self() # Combine all conditions with AllArbiter final_demand = AllArbiterDemandData( [time_before_arbiter, recipient_arbiter, any_arbiter], [deadline_demand, recipient_demand, oracle_or_demand] ).encode_self() # Create the escrow with nested logical arbiters (using approve_and_create) price = {"address": erc20_token, "value": 200} item = {"arbiter": all_arbiter, "demand": final_demand} escrow_receipt = await alice_client.erc20.escrow.non_tierable.approve_and_create( price, item, int(time.time()) + 7200 # 2 hour overall expiration ) ``` ```bash # Encode individual demands alkahest arbiter encode-demand --type time-before --time 1735693200 # → { "encoded": "0xDEADLINE_DEMAND" } alkahest arbiter encode-demand --type recipient --recipient 0xBOB # → { "encoded": "0xRECIPIENT_DEMAND" } alkahest arbiter encode-demand --type trusted-oracle --oracle 0xCHARLIE --data 0x # → { "encoded": "0xORACLE1_DEMAND" } alkahest arbiter encode-demand --type trusted-oracle --oracle 0xDAVE --data 0x # → { "encoded": "0xORACLE2_DEMAND" } # Create the OR condition for oracles alkahest arbiter encode-demand --type any \ --demands '[{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE1_DEMAND"},{"arbiter":"0xTRUSTED_ORACLE_ARBITER","demand":"0xORACLE2_DEMAND"}]' # → { "encoded": "0xORACLE_OR_DEMAND" } # Combine all conditions with AllArbiter: deadline AND recipient AND (oracle1 OR oracle2) alkahest arbiter encode-demand --type all \ --demands '[{"arbiter":"0xTIME_BEFORE_ARBITER","demand":"0xDEADLINE_DEMAND"},{"arbiter":"0xRECIPIENT_ARBITER","demand":"0xRECIPIENT_DEMAND"},{"arbiter":"0xANY_ARBITER","demand":"0xORACLE_OR_DEMAND"}]' # → { "encoded": "0xFINAL_DEMAND" } # Create the escrow with nested logical arbiters alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xERC20_TOKEN --amount 200000000000000000000 \ --arbiter 0xALL_ARBITER \ --demand 0xFINAL_DEMAND \ --expiration 1735700400 \ --approve ``` ## Parsing composed demands The CLI and SDKs provide built-in support for recursively parsing composed demands. The parsers automatically detect the arbiter type and decode each sub-demand appropriately. ```solidity // Example: Validate that on-chain demand matches expected structure function validateDemand( bytes memory onChainDemand, bytes memory expectedDemand ) public pure returns (bool) { // Simply check if the demands match return keccak256(onChainDemand) == keccak256(expectedDemand); } // Or decode and validate specific parts function parseAllArbiterDemand(bytes memory demand) public pure returns (address[] memory arbiters, bytes[] memory demands) { AllArbiter.DemandData memory demandData = abi.decode( demand, (AllArbiter.DemandData) ); return (demandData.arbiters, demandData.demands); } // Example usage (address[] memory arbiters, bytes[] memory subDemands) = parseAllArbiterDemand(composedDemand); require(arbiters[0] == address(timeBeforeArbiter), "First arbiter must be TimeBeforeArbiter"); require(arbiters[1] == address(recipientArbiter), "Second arbiter must be RecipientArbiter"); ``` ```typescript import { decodeDemandWithAddresses } from "alkahest-ts/utils/demandParsing"; // Use the built-in recursive demand parser with chain addresses const decoded = decodeDemandWithAddresses( { arbiter: allArbiter, demand: composedDemand }, client.addresses, ); // The result includes arbiter, decoded data, and children for logical arbiters console.log("Top-level arbiter:", decoded.arbiter); console.log("Decoded data:", decoded.decoded); // For logical arbiters (AllArbiter, AnyArbiter), children are recursively decoded if (decoded.children) { for (const child of decoded.children) { console.log("Child arbiter:", child.arbiter); console.log("Child data:", child.decoded); // Children can also have their own children for nested logical arbiters if (child.children) { console.log("Nested children:", child.children); } } } // Check if an arbiter was unknown (not in the registered decoders) if (decoded.isUnknown) { console.log("Unknown arbiter, raw demand:", decoded.decoded.raw); } ``` ```rust use alkahest_rs::contracts::arbiters::logical::AllArbiter as AllArbiterContract; // Decode AllArbiter demand data from raw bytes let demand_data: AllArbiterContract::DemandData = AllArbiterContract::DemandData::abi_decode(&composed_demand, true)?; // Use the built-in recursive decoder let decoded = alice_client .arbiters() .logical() .all() .decode(demand_data)?; // The result contains arbiters and recursively decoded demands println!("Arbiters: {:?}", decoded.arbiters); // Each demand is decoded into a typed enum variant for demand in &decoded.demands { match demand { DecodedDemand::TimeBeforeArbiter(data) => { println!("TimeBeforeArbiter: time={}", data.time); } DecodedDemand::RecipientArbiter(data) => { println!("RecipientArbiter: recipient={}", data.recipient); } DecodedDemand::TrustedOracle(data) => { println!("TrustedOracle: oracle={}", data.oracle); } DecodedDemand::AnyArbiter(nested) => { // Nested logical arbiters are also decoded println!("Nested AnyArbiter with {} sub-demands", nested.demands.len()); } DecodedDemand::AllArbiter(nested) => { println!("Nested AllArbiter with {} sub-demands", nested.demands.len()); } DecodedDemand::Unknown { arbiter, raw_data } => { println!("Unknown arbiter {}: {:?}", arbiter, raw_data); } _ => println!("Other demand type"), } } ``` ```python # Use the built-in recursive decoder decoded = alice_client.arbiters.logical.all.decode(composed_demand) # The result contains arbiters and recursively decoded demands print(f"Arbiters: {decoded.arbiters}") # Each demand has a type and optional raw data for demand in decoded.demands: print(f"Demand type: {demand.demand_type}") if demand.raw_data: # Raw data can be further decoded based on the demand type if demand.demand_type == "TimeBeforeArbiter": time_value = decode(['uint64'], demand.raw_data)[0] print(f" Time: {datetime.fromtimestamp(time_value)}") elif demand.demand_type == "RecipientArbiter": recipient = decode(['address'], demand.raw_data)[0] print(f" Recipient: {recipient}") elif demand.demand_type == "TrustedOracle": oracle, data = decode(['address', 'bytes'], demand.raw_data) print(f" Oracle: {oracle}") # For AnyArbiter, use the any decoder decoded_any = alice_client.arbiters.logical.any.decode(any_arbiter_demand) print(f"AnyArbiter sub-demands: {len(decoded_any.demands)}") ``` ```bash # Decode a composed demand given the arbiter address and encoded demand hex alkahest arbiter decode-demand \ --arbiter 0xALL_ARBITER \ --demand 0xCOMPOSED_DEMAND_HEX # → Recursively decoded demand structure as JSON ``` For custom arbiters not included in the SDK's built-in decoders, you can extend the decoder registry (TypeScript) or manually decode the raw bytes using ABI decoding. ## Why there's no NotArbiter You might expect a `NotArbiter` to complement `AllArbiter` and `AnyArbiter`, allowing you to negate any arbiter's logic. However, Alkahest intentionally does not include a `NotArbiter` due to fundamental security concerns. ### The revert problem Arbiters typically signal failure by **reverting** rather than returning `false`. This allows them to provide meaningful error messages explaining why arbitration failed (e.g., "recipient mismatch" or "deadline passed"). A `NotArbiter` would need to catch these reverts and invert the result, but this creates a critical ambiguity: **A `NotArbiter` cannot distinguish between:** - **Logical reversions** - The arbiter intentionally reverted because its condition wasn't met (e.g., wrong recipient) - **Execution reversions** - The call failed for technical reasons (e.g., out of gas, invalid calldata, contract bug) If a `NotArbiter` treated all reverts as "condition not met" and returned success, it would incorrectly pass arbitration when calls fail due to out-of-gas attacks, malformed data, or other execution errors. This could allow attackers to bypass security checks. ### What to do instead **Use built-in complement arbiters.** Most arbiters with useful complements already have them: | Arbiter | Complement | |---------|------------| | `TimeBeforeArbiter` | `TimeAfterArbiter` | | `ExpirationTimeBeforeArbiter` | `ExpirationTimeAfterArbiter` | **Write a standalone inverting arbiter.** Fork or rewrite the arbiter with inverted logic. This is the safest approach since you explicitly implement the complement: ```solidity // Example: Standalone arbiter that inverts RecipientArbiter logic // (allows anyone EXCEPT a specific address) contract NotRecipientArbiter is IArbiter { struct DemandData { address excludedRecipient; } function checkStatement( Attestation memory statement, bytes memory demand, bytes32 /* counteroffer */ ) external pure returns (bool) { DemandData memory data = abi.decode(demand, (DemandData)); // Explicitly check the inverse condition return statement.recipient != data.excludedRecipient; } } ``` **Subcall the original arbiter with explicit error handling.** If you want to wrap an existing arbiter, you must explicitly handle its known revert reasons rather than catching all reverts: ```solidity // Example: Inverting arbiter that subcalls the original contract InvertingArbiter is IArbiter { IArbiter public immutable underlying; constructor(IArbiter _underlying) { underlying = _underlying; } function checkStatement( Attestation memory statement, bytes memory demand, bytes32 counteroffer ) external view returns (bool) { try underlying.checkStatement(statement, demand, counteroffer) returns (bool result) { return !result; } catch (bytes memory reason) { // CRITICAL: Only catch KNOWN logical revert reasons // Unknown reverts (OOG, invalid data) must propagate bytes4 selector = bytes4(reason); if (selector == RecipientMismatch.selector) { return true; // Logical failure -> inverted success } if (selector == DeadlinePassed.selector) { return true; // Logical failure -> inverted success } // Unknown error - do NOT treat as success, propagate it assembly { revert(add(reason, 32), mload(reason)) } } } } ``` This approach requires you to enumerate every logical revert reason the underlying arbiter can produce. Unknown reverts (out of gas, invalid calldata, bugs) propagate rather than being incorrectly treated as "condition not met." --- ## pt 2 - Job Trading import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Escrow Flow (pt 2 - Job Trading) Here's how Alice buys a compute job from Bob with her ERC-20 token, where the validity of the job result is decided by a mutually trusted third party Charlie: 1. Alice deposits her ERC-20 token into escrow via the ERC20EscrowObligation contract. Her demand is made via the TrustedOracleArbiter contract, and contains her compute query and the mutually trusted third party's address (Charlie). 2. Bob parses Alice's query from her demand, completes the compute task, and submits the result in an attestation via the StringResultObligation contract. Then he requests an arbitration on his attestation from Charlie via TrustedOracleArbiter. 3. Charlie sees the arbitration request event, reads Alice's query and Bob's fulfillment, and determines whether the fulfillment is valid. 4. Finalization 1. If Charlie decides the fulfillment is valid, Bob can now use his result attestation from StringResultObligation to claim Alice's escrow. 2. If not valid, Alice can wait for her escrow to expire and reclaim it. > **Note on frontrunning:** In the flow below, Bob's fulfillment data (the capitalized string) is visible in his transaction. Since the result is self-contained — anyone could copy "HELLO WORLD" and submit it as their own fulfillment — a frontrunner watching the mempool could steal Bob's work. If your fulfillment data has this property, a commit-reveal mechanism can prevent this (documentation coming soon). ## Depositing escrow using TrustedOracleArbiter You can use TrustedOracleArbiter when you want an off-chain judge to decide whether a deal has been validly fulfilled. The judge could be a real person or an automated program. In this example, we'll demand that Bob capitalize a string for us, and Charlie will verify off-chain whether Bob did so correctly. In practice, this might represent Bob doing a complex computation, and Charlie verifying if it meets Alice's criteria. ```solidity // Encode the demand with TrustedOracleArbiter bytes memory demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: charlie, // Charlie's address as the trusted oracle data: abi.encode("capitalize hello world") // The job query }) ); // Approve and deposit ERC-20 into escrow IERC20(erc20Token).approve(address(erc20EscrowObligation), 100 * 10**18); bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: erc20Token, amount: 100 * 10**18, arbiter: address(trustedOracleArbiter), demand: demand }), block.timestamp + 86400 // 24 hour expiration ); ``` ```typescript import { encodeAbiParameters, parseAbiParameters } from "viem"; // Encode the demand with TrustedOracleArbiter const demand = aliceClient.arbiters.encodeTrustedOracleDemand({ oracle: charlie, // Charlie's address as the trusted oracle data: encodeAbiParameters(parseAbiParameters("(string query)"), [ { query: "capitalize hello world" }, ]), }); // Deposit ERC-20 into escrow with permit (gasless approval - single step) const { attested: escrow } = await aliceClient.erc20.escrow.nonTierable.permitAndCreate( { address: erc20Token, value: parseEther("100") }, { arbiter: trustedOracleArbiter, demand: demand }, BigInt(Math.floor(Date.now() / 1000) + 86400), // 24 hour expiration ); // Or with approveAndCreate (combines approval + deposit in one SDK call) const { attested: escrow } = await aliceClient.erc20.escrow.nonTierable.approveAndCreate( { address: erc20Token, value: parseEther("100") }, { arbiter: trustedOracleArbiter, demand: demand }, BigInt(Math.floor(Date.now() / 1000) + 86400), ); ``` ```rust use alloy::sol_types::SolValue; use alkahest_rs::sol_types::ArbiterDemand; // Encode the demand with TrustedOracleArbiter let demand_data = ArbiterDemand { oracle: charlie_address, demand: "capitalize hello world".abi_encode().into(), }; let demand = demand_data.abi_encode().into(); let price = Erc20Data { address: erc20_token, value: U256::from(100e18), }; let item = ArbiterData { arbiter: trusted_oracle_arbiter, demand: demand, }; // Deposit ERC-20 into escrow with permit (gasless approval - single step) let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .permit_and_create(&price, &item, expiration_time) .await?; let escrow_uid = DefaultAlkahestClient::get_attested_event(escrow_receipt)?.uid; // Or with approve_and_create (combines approval + deposit in one SDK call) let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .approve_and_create(&price, &item, expiration_time) .await?; ``` ```python from eth_abi import encode import time from alkahest_py import TrustedOracleArbiterDemandData # Encode the demand with TrustedOracleArbiter (address + abi-encoded inner bytes) inner = encode(['string'], ['capitalize hello world']) demand = TrustedOracleArbiterDemandData(charlie, inner).encode_self() price = {"address": erc20_token, "value": 100} item = {"arbiter": trusted_oracle_arbiter, "demand": demand} # Deposit ERC-20 into escrow with permit (gasless approval - single step) escrow_receipt = await alice_client.erc20.escrow.non_tierable.permit_and_create( price, item, int(time.time()) + 86400 # 24 hour expiration ) escrow_uid = escrow_receipt["log"]["uid"] # Or with approve_and_create (combines approval + deposit in one SDK call) escrow_receipt = await alice_client.erc20.escrow.non_tierable.approve_and_create( price, item, int(time.time()) + 86400 ) escrow_uid = escrow_receipt["log"]["uid"] ``` ```bash # Step 1: Encode the TrustedOracleArbiter demand alkahest arbiter encode-demand \ --type trusted-oracle \ --oracle 0xCHARLIE_ADDRESS \ --data 0x # extra data for the oracle (can embed the query) # Returns: { "success": true, "data": { "encoded": "0x..." } } # Step 2: Create the escrow with the encoded demand alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xERC20_TOKEN \ --amount 100000000000000000000 \ --arbiter 0xTRUSTED_ORACLE_ARBITER \ --demand 0xENCODED_DEMAND \ --expiration 1735689600 \ --approve # Returns: { "success": true, "data": { "uid": "0xESCROW_UID", ... } } ``` ## Fulfilling a job and requesting arbitration The `data` field of TrustedOracleArbiter doesn't enforce any kind of schema, so you have to come to an agreement with a buyer beforehand in order to understand and parse their query. StringObligation is a similarly flexible contract that you can use for fulfillments when the data won't be used directly by other on-chain contracts. Note that the fulfillment should contain a copy or reference to the demand it's intended to fulfill. This is needed to prevent a fulfillment that's valid for one query being used to claim another escrow demanding the same judge, but with a different query. Usually, this is done by setting the escrow attestation as the refUID of the fulfillment. ```solidity // Bob fulfills the job with the result bytes32 fulfillmentUid = stringObligation.doObligation( "HELLO WORLD", // The computed result (capitalized) escrowUid // Reference to the escrow being fulfilled ); // Bob requests arbitration from Charlie trustedOracleArbiter.requestArbitration(fulfillmentUid, charlie); ``` ```typescript // Bob fulfills the job with the result const { attested: fulfillment } = await bobClient.stringObligation.doObligation( "HELLO WORLD", // The computed result (capitalized) escrow.uid, // Reference to the escrow being fulfilled ); // Bob requests arbitration from Charlie await bobClient.oracle.requestArbitration(fulfillment.uid, charlie); ``` ```rust // Bob fulfills the job with the result let fulfillment_receipt = bob_client .string_obligation() .do_obligation( "HELLO WORLD", // The computed result (capitalized) escrow_uid, // Reference to the escrow being fulfilled ) .await?; let fulfillment_uid = get_attested_event(fulfillment_receipt)?.uid; // Bob requests arbitration from Charlie bob_client .oracle() .request_arbitration(fulfillment_uid, charlie_address) .await?; ``` ```python # Bob fulfills the job with the result fulfillment_uid = await bob_client.string_obligation.do_obligation( "HELLO WORLD", # The computed result (capitalized) escrow_uid # Reference to the escrow being fulfilled ) ``` ```bash # Bob: Submit the result as a string fulfillment referencing the escrow alkahest --private-key 0xBOB_KEY string create \ --item "HELLO WORLD" \ --ref-uid 0xESCROW_UID # Returns: { "success": true, "data": { "uid": "0xFULFILLMENT_UID", ... } } # Charlie (oracle): Arbitrate the fulfillment alkahest --private-key 0xCHARLIE_KEY arbiter arbitrate \ --obligation 0xFULFILLMENT_UID \ --demand 0xDEMAND_HEX \ --decision true ``` Charlie should listen for `ArbitrationRequested` events demanding him as the oracle. Then he can retrieve the attestation from EAS, decide if it's valid, and use the arbitrate function on TrustedOracleArbiter to submit his decision on-chain. Note that the `ArbitrationRequested` event doesn't have a reference to an escrow/demand. As explained above, the demand should be retrieved from the fulfillment attestation. If it's an escrow attestation referenced by refUID, you can get the attestation from EAS and parse the demand. ```solidity // Charlie listens for ArbitrationRequested events (off-chain) // When an event is found, Charlie retrieves the fulfillment attestation Attestation memory fulfillment = eas.getAttestation(fulfillmentUid); // Get the escrow attestation from the fulfillment's refUID Attestation memory escrow = eas.getAttestation(fulfillment.refUID); // Parse the demand to get the original query TrustedOracleArbiter.DemandData memory demandData = abi.decode( escrow.data, (TrustedOracleArbiter.DemandData) ); string memory query = abi.decode(demandData.data, (string)); // Parse the fulfillment result string memory result = abi.decode(fulfillment.data, (string)); // Charlie validates the result bool isValid = false; if (bytes(query).length > 10 && keccak256(bytes(substring(query, 0, 10))) == keccak256(bytes("capitalize"))) { // Extract the text to capitalize (everything after "capitalize ") string memory textToCapitalize = substring(query, 11, bytes(query).length); // Convert to uppercase and compare isValid = keccak256(bytes(result)) == keccak256(bytes(toUpperCase(textToCapitalize))); } trustedOracleArbiter.arbitrate(fulfillmentUid, isValid); ``` ```typescript // Charlie sets up an automatic oracle const { unwatch } = await charlieClient.oracle.listenAndArbitrateForEscrow({ escrow: { attester: addresses.erc20EscrowObligation, demandAbi: parseAbiParameters("(string query)"), }, fulfillment: { attester: addresses.stringObligation, obligationAbi: parseAbiParameters("(string item)"), }, arbitrate: async (obligation, demand) => { // Parse the query and result const query = demand[0].query; const result = obligation[0].item; // Check if this is a capitalization query if (query.startsWith("capitalize ")) { // Extract the text that should be capitalized const textToCapitalize = query.substring(11); // Validate: did Bob capitalize the string correctly? const expectedResult = textToCapitalize.toUpperCase(); return result === expectedResult; } // Unknown query type return false; }, onAfterArbitrate: async (decision) => { console.log(`Arbitrated ${decision.attestation.uid}: ${decision.decision}`); }, pollingInterval: 1000, }); ``` ```rust use alkahest_rs::oracle::{ArbitrateOptions, EscrowParams, FulfillmentParams}; // Charlie sets up an automatic oracle let result = charlie_client .oracle() .listen_and_arbitrate_for_escrow_sync( &EscrowParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(erc20_escrow_obligation)), ..Default::default() }, _demand_data: PhantomData::, }, &FulfillmentParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(string_obligation)), ..Default::default() }, _obligation_data: PhantomData::, }, |obligation: &StringObligation, demand: &QueryDemand| { // Parse the query and result let query = &demand.query; let result = &obligation.item; // Check if this is a capitalization query if query.starts_with("capitalize ") { // Extract the text that should be capitalized let text_to_capitalize = &query[11..]; // Validate: did Bob capitalize the string correctly? let expected_result = text_to_capitalize.to_uppercase(); Some(result == &expected_result) } else { // Unknown query type Some(false) } }, |decision| async move { println!( "Arbitrated {}: {}", decision.attestation.uid, decision.decision ); }, &ArbitrateOptions::default(), ) .await?; ``` ```python # Charlie sets up an automatic oracle using the Python SDK from eth_abi import decode as abi_decode from alkahest_py import ( AttestationFilter, FulfillmentParams, EscrowParams, ArbitrateOptions, StringObligationData, TrustedOracleArbiterDemandData, ) # Build filters/params escrow_filter = AttestationFilter( attester=addresses["erc20EscrowObligation"], recipient=None, schema_uid=None, uid=None, ref_uid=None, from_block=0, to_block=None, ) fulfillment_filter = AttestationFilter( attester=addresses["stringObligation"], recipient=None, schema_uid=None, uid=None, ref_uid=None, from_block=0, to_block=None, ) # Decoder helpers obligation_abi = StringObligationData(item="") fulfillment_params = FulfillmentParams(obligation_abi, fulfillment_filter) # Demand ABI: pass an encoded TrustedOracleArbiter demand template so the SDK knows how to decode the outer struct. # The inner "data" field (bytes) will be decoded below in the decision function. demand_abi = TrustedOracleArbiterDemandData(charlie, b"").encode_self() escrow_params = EscrowParams(demand_abi, escrow_filter) options = ArbitrateOptions(require_oracle=True, skip_arbitrated=False, require_request=False, only_new=False) def validate_job(obligation_str, demand_data): # obligation_str is the decoded string from StringObligation # demand_data is a TrustedOracleArbiterDemandData object with fields: oracle, data (bytes) try: (query,) = abi_decode(['string'], demand_data.data) except Exception: return False if query.startswith("capitalize "): text_to_capitalize = query[11:] expected = text_to_capitalize.upper() return obligation_str == expected return False unwatch = await charlie_client.oracle.listen_and_arbitrate_for_escrow_no_spawn( escrow_params, fulfillment_params, validate_job, lambda decision: print(f"Arbitrated {decision.attestation.uid}: {decision.decision}"), options, 1000, ) ``` ## Waiting for arbitration and claiming escrow Bob can listen for the `ArbitrationMade` event from TrustedOracleArbiter, then claim the escrow if the decision was positive, or retry if not. ```solidity // Bob listens for ArbitrationMade events (off-chain) // Filter for events related to Bob's fulfillment // When a positive decision is found: // Bob claims the escrow using the fulfillment attestation erc20EscrowObligation.collectEscrow(escrowUid, fulfillmentUid); ``` ```typescript // Bob listens for arbitration decision const filter = await publicClient.createEventFilter({ address: trustedOracleArbiter, event: parseAbiEvent( "event ArbitrationMade(bytes32 indexed obligation, address indexed oracle, bool decision)", ), args: { obligation: fulfillment.uid, }, }); const checkArbitration = async () => { const logs = await publicClient.getFilterLogs({ filter }); if (logs.length > 0 && logs[0].args.decision) { // Positive decision - Bob claims the escrow await bobClient.erc20.escrow.nonTierable.collect(escrow.uid, fulfillment.uid); return true; } return false; }; // Poll for arbitration const result = await checkArbitration(); if (!result) { console.log("Arbitration failed, consider retrying"); } ``` ```rust use alloy::rpc::types::Filter; use alloy::sol_types::SolEvent; // Bob listens for arbitration decision let filter = Filter::new() .address(trusted_oracle_arbiter_address) .event_signature(TrustedOracleArbiter::ArbitrationMade::SIGNATURE_HASH) .topic1(fulfillment_uid); let logs = provider.get_logs(&filter).await?; for log in logs { let event = log.log_decode::()?; if event.inner.decision { // Positive decision - Bob claims the escrow bob_client .erc20() .escrow() .non_tierable() .collect(escrow_uid, fulfillment_uid) .await?; println!("Successfully claimed escrow!"); } else { println!("Arbitration failed, consider retrying"); } } ``` ```python # Bob listens for arbitration decision async def check_arbitration(): logs = await public_client.get_logs({ "address": trusted_oracle_arbiter, "topics": [ Web3.keccak(text="ArbitrationMade(bytes32,address,bool)"), fulfillment_uid, ], }) if logs: decision = decode_log(logs[0], "ArbitrationMade")["decision"] if decision: # Positive decision - Bob claims the escrow tx_hash = await bob_client.erc20.escrow.non_tierable.collect( escrow_uid, fulfillment_uid, ) print(f"Successfully claimed escrow! TX: {tx_hash}") return True else: print("Arbitration failed, consider retrying") return False return None # Poll for arbitration result = await check_arbitration() ``` ```bash # Bob: Collect the escrow after successful arbitration alkahest --private-key 0xBOB_KEY escrow collect \ --erc20 \ --escrow-uid 0xESCROW_UID \ --fulfillment-uid 0xFULFILLMENT_UID # Or wait for fulfillment (blocks until someone fulfills and collects) alkahest --private-key 0xALICE_KEY escrow wait \ --erc20 --uid 0xESCROW_UID ``` If no valid fulfillment is made, Alice can reclaim the escrow after it expires. ```solidity // Alice reclaims her expired escrow erc20EscrowObligation.reclaimExpired(escrowUid); ``` ```typescript // Alice reclaims her expired escrow await aliceClient.erc20.escrow.nonTierable.reclaimExpired(escrow.uid); ``` ```rust // Alice reclaims her expired escrow alice_client .erc20() .escrow() .non_tierable() .reclaim_expired(escrow_uid) .await?; ``` ```python # Alice reclaims her expired escrow await alice_client.erc20.escrow.non_tierable.reclaim_expired(escrow_uid) ``` ```bash # Alice reclaims her expired escrow alkahest --private-key 0xALICE_KEY escrow reclaim \ --erc20 --uid 0xESCROW_UID ``` ## SDK utilities The SDK provides functions to make it easier to set up an automatic oracle service that interacts with TrustedOracleArbiter. ```typescript // Set up an automatic oracle that validates string capitalization const { unwatch } = await charlieClient.oracle.listenAndArbitrate({ fulfillment: { attester: stringObligation, obligationAbi: parseAbiParameters("(string item)"), }, arbitrate: async (obligation) => { // Custom validation logic for any capitalization task const result = obligation[0].item; // Check if result is all uppercase (simple heuristic) const isCapitalized = result === result.toUpperCase() && result.length > 0; return isCapitalized ? true : false; }, onAfterArbitrate: async (decision) => { console.log(`Arbitrated ${decision.attestation.uid}: ${decision.decision}`); }, pollingInterval: 1000, // Check every second }); // For more complex validation with access to the original demand const { unwatch: unwatchEscrow } = await charlieClient.oracle.listenAndArbitrateForEscrow({ escrow: { attester: erc20EscrowObligation, demandAbi: parseAbiParameters("(string query)"), }, fulfillment: { attester: stringObligation, obligationAbi: parseAbiParameters("(string item)"), }, arbitrate: async (obligation, demand) => { // Validate that the fulfillment matches the demand const query = demand[0].query; const result = obligation[0].item; // Support different query types if (query.startsWith("capitalize ")) { const textToCapitalize = query.substring(11); return result === textToCapitalize.toUpperCase(); } else if (query.startsWith("reverse ")) { const textToReverse = query.substring(8); return result === textToReverse.split("").reverse().join(""); } else if (query.startsWith("length ")) { const textToMeasure = query.substring(7); return result === textToMeasure.length.toString(); } return false; }, onAfterArbitrate: async (decision) => { console.log(`Validated job: ${decision.decision}`); }, pollingInterval: 1000, }); // Arbitrate past fulfillments (useful for catching up) const decisions = await charlieClient.oracle.arbitratePast({ fulfillment: { attester: stringObligation, obligationAbi: parseAbiParameters("(string item)"), }, arbitrate: async (obligation) => { return obligation[0].item === obligation[0].item.toUpperCase(); }, skipAlreadyArbitrated: true, // Don't re-arbitrate }); ``` ```rust use alloy::dyn_abi::SolType; use alloy::sol_types::sol; sol! { struct StringObligation { string item; } struct QueryDemand { string query; } } // Set up an automatic oracle that validates string capitalization let result = charlie_client .oracle() .listen_and_arbitrate_sync( &FulfillmentParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(string_obligation_address)), ..Default::default() }, _obligation_data: PhantomData::, }, &|obligation: &StringObligation| { // Custom validation logic for any capitalization task let result = &obligation.item; // Check if result is all uppercase (simple heuristic) let is_capitalized = !result.is_empty() && result == &result.to_uppercase(); Some(is_capitalized) }, |decision| async move { println!("Arbitrated {}: {}", decision.attestation.uid, decision.decision); }, &ArbitrateOptions::default(), ) .await?; // For more complex validation with access to the original demand let escrow_result = charlie_client .oracle() .listen_and_arbitrate_for_escrow_sync( &EscrowParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(escrow_obligation_address)), ..Default::default() }, _demand_data: PhantomData::, }, &FulfillmentParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(string_obligation_address)), ..Default::default() }, _obligation_data: PhantomData::, }, |obligation: &StringObligation, demand: &QueryDemand| { // Validate that the fulfillment matches the demand let query = &demand.query; let result = &obligation.item; // Support different query types if query.starts_with("capitalize ") { let text_to_capitalize = &query[11..]; Some(result == &text_to_capitalize.to_uppercase()) } else if query.starts_with("reverse ") { let text_to_reverse = &query[8..]; let reversed: String = text_to_reverse.chars().rev().collect(); Some(result == &reversed) } else if query.starts_with("length ") { let text_to_measure = &query[7..]; Some(result == &text_to_measure.len().to_string()) } else { Some(false) } }, |decision| async move { println!("Validated job: {}", decision.decision); }, &ArbitrateOptions::default(), ) .await?; // Arbitrate past fulfillments (useful for catching up) let decisions = charlie_client .oracle() .arbitrate_past_sync( &FulfillmentParams { filter: AttestationFilter { attester: Some(ValueOrArray::Value(string_obligation_address)), ..Default::default() }, _obligation_data: PhantomData::, }, &|obligation: &StringObligation| { Some(obligation.item == obligation.item.to_uppercase()) }, &ArbitrateOptions { skip_arbitrated: true, // Don't re-arbitrate ..Default::default() }, ) .await?; ``` ```python # Set up an automatic oracle that validates string capitalization from alkahest_py import AttestationFilter, FulfillmentParams, ArbitrateOptions, StringObligationData def validate_capitalization(obligation_str): """Custom validation logic for any capitalization task""" return len(obligation_str) > 0 and obligation_str == obligation_str.upper() fulfillment_filter = AttestationFilter( attester=string_obligation, recipient=None, schema_uid=None, uid=None, ref_uid=None, from_block=0, to_block=None, ) obligation_abi = StringObligationData(item="") fulfillment_params = FulfillmentParams(obligation_abi, fulfillment_filter) options = ArbitrateOptions(require_oracle=True, skip_arbitrated=False, require_request=False, only_new=False) unwatch = await charlie_client.oracle.listen_and_arbitrate_no_spawn( fulfillment_params, validate_capitalization, lambda decision: print(f"Arbitrated {decision.attestation.uid}: {decision.decision}"), options, 1000, # Check every second ) # For more complex validation with access to the original demand from eth_abi import decode as abi_decode from alkahest_py import EscrowParams, TrustedOracleArbiterDemandData def validate_with_demand(obligation_str, demand_data): """Validate that the fulfillment matches the demand""" # demand_data is a TrustedOracleArbiterDemandData object with fields: oracle, data (bytes) try: (query,) = abi_decode(['string'], demand_data.data) except Exception: return False if query.startswith("capitalize "): text_to_capitalize = query[11:] return obligation_str == text_to_capitalize.upper() elif query.startswith("reverse "): text_to_reverse = query[8:] return obligation_str == text_to_reverse[::-1] elif query.startswith("length "): text_to_measure = query[7:] return obligation_str == str(len(text_to_measure)) return False escrow_filter = AttestationFilter( attester=erc20_escrow_obligation, recipient=None, schema_uid=None, uid=None, ref_uid=None, from_block=0, to_block=None, ) fulfillment_filter = AttestationFilter( attester=string_obligation, recipient=None, schema_uid=None, uid=None, ref_uid=None, from_block=0, to_block=None, ) demand_abi = TrustedOracleArbiterDemandData(charlie, b"").encode_self() escrow_params = EscrowParams(demand_abi, escrow_filter) obligation_abi = StringObligationData(item="") fulfillment_params = FulfillmentParams(obligation_abi, fulfillment_filter) unwatch_escrow = await charlie_client.oracle.listen_and_arbitrate_for_escrow_no_spawn( escrow_params, fulfillment_params, validate_with_demand, lambda decision: print(f"Validated job: {decision.decision}"), options, 1000, ) # Arbitrate past fulfillments (useful for catching up) decisions = await charlie_client.oracle.arbitrate_past_sync( fulfillment_params, lambda obligation_str: obligation_str == obligation_str.upper(), ArbitrateOptions(require_oracle=True, skip_arbitrated=True, require_request=False, only_new=False), ) ``` It also provides functions for waiting for an escrow to be fulfilled (claimed), and for listening for the ArbitrationMade event. ```typescript // Alice waits for her escrow to be fulfilled const fulfillmentResult = await aliceClient.waitForFulfillment( aliceClient.contractAddresses.erc20EscrowObligation, escrow.uid, 1000, // Optional polling interval ); if (fulfillmentResult.fulfillment) { console.log(`Escrow fulfilled by ${fulfillmentResult.fulfiller}`); console.log(`Fulfillment UID: ${fulfillmentResult.fulfillment}`); } // Bob waits for arbitration and then claims const waitForArbitrationAndClaim = async () => { // Wait for arbitration decision return new Promise((resolve) => { const unwatch = publicClient.watchEvent({ address: trustedOracleArbiter, event: parseAbiEvent( "event ArbitrationMade(bytes32 indexed obligation, address indexed oracle, bool decision)", ), args: { obligation: fulfillment.uid }, onLogs: async (logs) => { if (logs[0].args.decision) { // Positive decision - claim the escrow await bobClient.erc20.escrow.nonTierable.collect(escrow.uid, fulfillment.uid); resolve({ success: true }); } else { resolve({ success: false }); } unwatch(); }, pollingInterval: 1000, }); }); }; const result = await waitForArbitrationAndClaim(); ``` ```rust // Alice waits for her escrow to be fulfilled let fulfillment_result = alice_client .wait_for_fulfillment( erc20_escrow_obligation_address, escrow_uid, None, // Use default block to start from ) .await?; match fulfillment_result { Ok(log) => { println!("Escrow fulfilled!"); println!("Fulfillment UID: {}", log.args.fulfillment); println!("Fulfiller: {}", log.args.fulfiller); } Err(e) => { println!("Error waiting for fulfillment: {}", e); } } // Bob waits for arbitration and then claims use futures::StreamExt; let filter = Filter::new() .address(trusted_oracle_arbiter_address) .event_signature(TrustedOracleArbiter::ArbitrationMade::SIGNATURE_HASH) .topic1(fulfillment_uid); let sub = provider.subscribe_logs(&filter).await?; let mut stream = sub.into_stream(); while let Some(log) = stream.next().await { let event = log.log_decode::()?; if event.inner.decision { // Positive decision - claim the escrow bob_client .erc20() .escrow() .non_tierable() .collect(escrow_uid, fulfillment_uid) .await?; println!("Successfully claimed escrow!"); break; } else { println!("Arbitration failed"); break; } } ``` ```python import asyncio import time # Alice waits for her escrow to be fulfilled async def wait_for_fulfillment(escrow_uid, timeout=60): """Wait for an escrow to be fulfilled""" start_time = time.time() while time.time() - start_time < timeout: # Check for EscrowCollected events logs = await public_client.get_logs({ "address": erc20_escrow_obligation, "topics": [ Web3.keccak(text="EscrowCollected(bytes32,bytes32,address)"), escrow_uid, ], "fromBlock": "earliest", }) if logs: event_data = decode_log(logs[0], "EscrowCollected") return { "fulfillment": event_data["fulfillment"], "fulfiller": event_data["fulfiller"], "success": True } await asyncio.sleep(1) raise TimeoutError("Timeout waiting for fulfillment") # Alice waits for her escrow to be fulfilled fulfillment_result = await wait_for_fulfillment(escrow_uid) if fulfillment_result["success"]: print(f"Escrow fulfilled by {fulfillment_result['fulfiller']}") print(f"Fulfillment UID: {fulfillment_result['fulfillment']}") # Bob waits for arbitration and then claims async def wait_for_arbitration_and_claim(fulfillment_uid, escrow_uid, timeout=60): """Wait for arbitration decision and claim if positive""" start_time = time.time() while time.time() - start_time < timeout: # Check for ArbitrationMade events logs = await public_client.get_logs({ "address": trusted_oracle_arbiter, "topics": [ Web3.keccak(text="ArbitrationMade(bytes32,address,bool)"), fulfillment_uid, ], "fromBlock": "latest", }) if logs: decision = decode_log(logs[0], "ArbitrationMade")["decision"] if decision: # Positive decision - claim the escrow tx_hash = await bob_client.erc20.escrow.non_tierable.collect(escrow_uid, fulfillment_uid) return {"success": True, "tx_hash": tx_hash} else: return {"success": False, "reason": "Negative arbitration"} await asyncio.sleep(1) raise TimeoutError("Timeout waiting for arbitration") # Bob waits and claims result = await wait_for_arbitration_and_claim( fulfillment_uid, escrow_uid, ) if result["success"]: print(f"Successfully claimed escrow! TX: {result['tx_hash']}") else: print(f"Failed to claim: {result['reason']}") ``` --- ## pt 1 - Token Trading import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Escrow Flow (pt 1 - Token Trading) Here's the process for Alice to buy an ERC-721 token from Bob with her ERC-20 token: 1. Alice deposits her ERC-20 token into escrow via the ERC20EscrowObligation contract, demanding the ERC-721 token she wants 2. Bob sends the ERC-721 token to Alice via the ERC721PaymentObligation contract, receiving an EAS attestation that he did so 3. Bob uses the attestation to claim Alice's escrow ## Depositing escrow First, make your demand with ERC721PaymentObligation.ObligationData. ```solidity // Encode the demand for the ERC-721 token Alice wants bytes memory demand = abi.encode( ERC721PaymentObligation.ObligationData({ token: 0x1234..., // ERC-721 token address tokenId: 42, // Token ID Alice wants payee: alice // Alice's address }) ); ``` ```typescript import { encodeAbiParameters, parseAbiParameters } from "viem"; // Encode the demand for the ERC-721 token Alice wants const demand = encodeAbiParameters( parseAbiParameters("(address token, uint256 tokenId, address payee)"), [ { token: "0x1234...", // ERC-721 token address tokenId: 42n, // Token ID Alice wants payee: alice, // Alice's address }, ], ); ``` ```rust use alloy::sol_types::SolValue; // Define the demand struct let demand_data = contracts::ERC721PaymentObligation::ObligationData { token: token_address, tokenId: U256::from(42), payee: alice_address, }; // Encode the demand let demand = demand_data.abi_encode().into(); ``` Then, approve the ERC20EscrowObligation contract to spend the token you're paying with, and deposit it into escrow using ERC20EscrowObligation.doObligation. ```solidity // Approve the escrow contract to spend Alice's ERC-20 tokens IERC20(erc20Token).approve(address(erc20EscrowObligation), 1000 * 10**18); // Deposit into escrow with the demand bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: erc20Token, amount: 1000 * 10**18, arbiter: address(erc721PaymentObligation), demand: demand }), block.timestamp + 86400 // 24 hour expiration ); ``` ```typescript // Approve the escrow contract to spend Alice's ERC-20 tokens await walletClient.writeContract({ address: erc20Token, abi: erc20Abi, functionName: "approve", args: [erc20EscrowObligation, parseEther("1000")], }); // Deposit into escrow with the demand const hash = await walletClient.writeContract({ address: erc20EscrowObligation, abi: erc20EscrowAbi, functionName: "doObligation", args: [ { token: erc20Token, amount: parseEther("1000"), arbiter: erc721PaymentObligation, demand: demand, }, BigInt(Math.floor(Date.now() / 1000) + 86400), ], }); ``` ```rust // Approve the escrow contract to spend Alice's ERC-20 tokens let token_contract = contracts::IERC20::new(token_address, &wallet_provider); token_contract .approve(escrow_obligation_address, U256::from(1000e18)) .send() .await? .get_receipt() .await?; // Deposit into escrow with the demand let escrow_contract = contracts::ERC20EscrowObligation::new( escrow_obligation_address, &wallet_provider ); let receipt = escrow_contract .doObligation( contracts::ERC20EscrowObligation::ObligationData { token: token_address, amount: U256::from(1000e18), arbiter: payment_obligation_address, demand: demand, }, expiration_time, ) .send() .await? .get_receipt() .await?; ``` If you want to buy a different kind of token, use the ObligationData struct from the corresponding PaymentObligation contract instead (e.g. ERC20PaymentObligation for buying an ERC-20 token, or TokenBundlePaymentObligation for a bundle of many token types). ```solidity // Example: Demanding ERC-20 payment instead bytes memory erc20Demand = abi.encode( ERC20PaymentObligation.ObligationData({ token: 0x5678..., amount: 500 * 10**18, payee: alice }) ); // Example: Demanding a token bundle bytes memory bundleDemand = abi.encode( TokenBundlePaymentObligation.ObligationData({ erc20Tokens: [erc20_1, erc20_2], erc20Amounts: [100, 200], erc721Tokens: [erc721_1], erc721TokenIds: [1], erc1155Tokens: [erc1155_1], erc1155TokenIds: [10], erc1155Amounts: [5], payee: alice }) ); ``` ```typescript // Example: Demanding ERC-20 payment instead const erc20Demand = encodeAbiParameters( parseAbiParameters("(address token, uint256 amount, address payee)"), [ { token: "0x5678...", amount: parseEther("500"), payee: alice, }, ], ); // Example: Demanding a token bundle const bundleDemand = encodeAbiParameters( parseAbiParameters( "(address[] erc20Tokens, uint256[] erc20Amounts, address[] erc721Tokens, uint256[] erc721TokenIds, address[] erc1155Tokens, uint256[] erc1155TokenIds, uint256[] erc1155Amounts, address payee)", ), [ { erc20Tokens: [erc20_1, erc20_2], erc20Amounts: [100n, 200n], erc721Tokens: [erc721_1], erc721TokenIds: [1n], erc1155Tokens: [erc1155_1], erc1155TokenIds: [10n], erc1155Amounts: [5n], payee: alice, }, ], ); ``` ```rust // Example: Demanding ERC-20 payment instead let erc20_demand_data = contracts::ERC20PaymentObligation::ObligationData { token: token_address, amount: U256::from(500e18), payee: alice_address, }; let erc20_demand = erc20_demand_data.abi_encode().into(); // Example: Demanding a token bundle let bundle_demand_data = contracts::TokenBundlePaymentObligation2::ObligationData { erc20Tokens: vec![erc20_1, erc20_2], erc20Amounts: vec![U256::from(100), U256::from(200)], erc721Tokens: vec![erc721_1], erc721TokenIds: vec![U256::from(1)], erc1155Tokens: vec![erc1155_1], erc1155TokenIds: vec![U256::from(10)], erc1155Amounts: vec![U256::from(5)], payee: alice_address, }; let bundle_demand = bundle_demand_data.abi_encode().into(); ``` The CLI's `barter create` command wraps the barter utility contracts to combine escrow + demand in one step. For manual escrow creation, use `escrow create`. ```bash # Alice: Create ERC20 escrow demanding an ERC721 token # First, get the ERC721PaymentObligation address for the demand arbiter alkahest config show --chain base-sepolia # Use the erc721PaymentObligation address as --arbiter # The demand is the ABI-encoded ERC721PaymentObligation.ObligationData # (token address, tokenId, payee=Alice) # You can construct this manually or use the SDK for encoding # Create the escrow alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xERC20_TOKEN \ --amount 1000000000000000000000 \ --arbiter 0xERC721_PAYMENT_OBLIGATION \ --demand 0xENCODED_DEMAND \ --expiration 1735689600 \ --approve ``` Or use barter utilities for atomic swaps (recommended for simple token-for-token trades): ```bash # Alice: Offer ERC20 for ERC20 (most common case) alkahest --private-key 0xALICE_KEY barter create \ --bid-type erc20 --ask-type erc20 \ --bid-token 0xUSDC --bid-amount 1000000000 \ --ask-token 0xEURC --ask-amount 900000000 \ --expiration 1735689600 # Alice: Offer ERC20 for ERC721 alkahest --private-key 0xALICE_KEY barter create \ --bid-type erc20 --ask-type erc721 \ --bid-token 0xUSDC --bid-amount 1000000000 \ --ask-token 0xNFT --ask-amount 0 --ask-token-id 42 \ --expiration 1735689600 ``` If you want to pay with a different kind of token, use the corresponding EscrowObligation contract instead when calling doObligation. Remember to approve the EscrowObligation contract to spend the token you're paying with (via setApprovalForAll for ERC-1155), and to approve all tokens in the bundle if you're using TokenBundleEscrowObligation. ```solidity // Example: Escrowing an ERC-721 token IERC721(erc721Token).approve(address(erc721EscrowObligation), tokenId); bytes32 escrowUid = erc721EscrowObligation.doObligation( ERC721EscrowObligation.ObligationData({ token: erc721Token, tokenId: tokenId, arbiter: address(erc20PaymentObligation), demand: erc20Demand }), expirationTime ); // Example: Escrowing ERC-1155 tokens IERC1155(erc1155Token).setApprovalForAll(address(erc1155EscrowObligation), true); bytes32 escrowUid = erc1155EscrowObligation.doObligation( ERC1155EscrowObligation.ObligationData({ token: erc1155Token, tokenId: tokenId, amount: 10, arbiter: address(erc20PaymentObligation), demand: erc20Demand }), expirationTime ); ``` ```typescript // Example: Escrowing an ERC-721 token await walletClient.writeContract({ address: erc721Token, abi: erc721Abi, functionName: "approve", args: [erc721EscrowObligation, tokenId], }); const hash = await walletClient.writeContract({ address: erc721EscrowObligation, abi: erc721EscrowAbi, functionName: "doObligation", args: [ { token: erc721Token, tokenId: tokenId, arbiter: erc20PaymentObligation, demand: erc20Demand, }, expirationTime, ], }); // Example: Escrowing ERC-1155 tokens await walletClient.writeContract({ address: erc1155Token, abi: erc1155Abi, functionName: "setApprovalForAll", args: [erc1155EscrowObligation, true], }); ``` ```rust // Example: Escrowing an ERC-721 token let erc721_contract = contracts::IERC721::new(erc721_address, &wallet_provider); erc721_contract .approve(escrow_obligation_address, token_id) .send() .await? .get_receipt() .await?; let escrow_contract = contracts::ERC721EscrowObligation::new( escrow_obligation_address, &wallet_provider ); let receipt = escrow_contract .doObligation( contracts::ERC721EscrowObligation::ObligationData { token: erc721_address, tokenId: token_id, arbiter: payment_obligation_address, demand: erc20_demand, }, expiration_time, ) .send() .await? .get_receipt() .await?; // Example: Escrowing ERC-1155 tokens let erc1155_contract = contracts::IERC1155::new(erc1155_address, &wallet_provider); erc1155_contract .setApprovalForAll(escrow_obligation_address, true) .send() .await? .get_receipt() .await?; ``` ## Fulfilling and claiming escrow You can fulfill demands for ERC-721 payments with ERC721PaymentObligation.doObligation. Remember to approve the contract to use your token first. Calling doObligation will return an attestation UID that you can use to claim the escrow. ```solidity // Bob approves the payment contract to transfer his ERC-721 IERC721(erc721Token).approve(address(erc721PaymentObligation), tokenId); // Bob fulfills Alice's demand bytes32 paymentUid = erc721PaymentObligation.doObligation( ERC721PaymentObligation.ObligationData({ token: erc721Token, tokenId: tokenId, payee: alice }) ); // Bob claims Alice's escrow using both attestation UIDs erc20EscrowObligation.collectEscrow(escrowUid, paymentUid); ``` ```typescript // Bob approves the payment contract to transfer his ERC-721 await walletClient.writeContract({ address: erc721Token, abi: erc721Abi, functionName: "approve", args: [erc721PaymentObligation, tokenId], }); // Bob fulfills Alice's demand const paymentTx = await walletClient.writeContract({ address: erc721PaymentObligation, abi: erc721PaymentAbi, functionName: "doObligation", args: [ { token: erc721Token, tokenId: tokenId, payee: alice, }, ], }); const paymentReceipt = await publicClient.waitForTransactionReceipt({ hash: paymentTx, }); const paymentUid = getAttestedEventFromReceipt(paymentReceipt).uid; // Bob claims Alice's escrow using both attestation UIDs await walletClient.writeContract({ address: erc20EscrowObligation, abi: erc20EscrowAbi, functionName: "collectEscrow", args: [escrowUid, paymentUid], }); ``` ```rust // Bob approves the payment contract to transfer his ERC-721 let erc721_contract = contracts::IERC721::new(erc721_address, &wallet_provider); erc721_contract .approve(payment_obligation_address, token_id) .send() .await? .get_receipt() .await?; // Bob fulfills Alice's demand let payment_contract = contracts::ERC721PaymentObligation::new( payment_obligation_address, &wallet_provider ); let payment_receipt = payment_contract .doObligation(contracts::ERC721PaymentObligation::ObligationData { token: erc721_address, tokenId: token_id, payee: alice_address, }) .send() .await? .get_receipt() .await?; let payment_uid = get_attested_event(payment_receipt)?.uid; // Bob claims Alice's escrow using both attestation UIDs let escrow_contract = contracts::ERC20EscrowObligation::new( escrow_obligation_address, &wallet_provider ); escrow_contract .collectEscrow(escrow_uid, payment_uid) .send() .await? .get_receipt() .await?; ``` ```bash # Bob: Fulfill a barter escrow (counterparty pays) alkahest --private-key 0xBOB_KEY barter fulfill \ --uid 0xESCROW_UID \ --bid-type erc20 --ask-type erc20 # Or for manual escrow collection (after separately creating a payment): alkahest --private-key 0xBOB_KEY escrow collect \ --erc20 \ --escrow-uid 0xESCROW_UID \ --fulfillment-uid 0xPAYMENT_UID ``` Use the corresponding PaymentObligation contract to fulfill demands for other tokens. ```solidity // Example: Fulfilling ERC-20 payment demand IERC20(erc20Token).approve(address(erc20PaymentObligation), amount); bytes32 paymentUid = erc20PaymentObligation.doObligation( ERC20PaymentObligation.ObligationData({ token: erc20Token, amount: amount, payee: alice }) ); // Example: Fulfilling token bundle payment demand // Approve all tokens in the bundle first for (uint i = 0; i < erc20Tokens.length; i++) { IERC20(erc20Tokens[i]).approve(address(bundlePaymentObligation), erc20Amounts[i]); } for (uint i = 0; i < erc721Tokens.length; i++) { IERC721(erc721Tokens[i]).approve(address(bundlePaymentObligation), erc721TokenIds[i]); } // ... then fulfill bytes32 paymentUid = bundlePaymentObligation.doObligation(bundleData); ``` ```typescript // Example: Fulfilling ERC-20 payment demand await walletClient.writeContract({ address: erc20Token, abi: erc20Abi, functionName: "approve", args: [erc20PaymentObligation, amount], }); const paymentTx = await walletClient.writeContract({ address: erc20PaymentObligation, abi: erc20PaymentAbi, functionName: "doObligation", args: [ { token: erc20Token, amount: amount, payee: alice, }, ], }); // Example: Fulfilling token bundle payment demand // Approve all tokens in the bundle first for (const token of bundleData.erc20Tokens) { await walletClient.writeContract({ address: token.address, abi: erc20Abi, functionName: "approve", args: [bundlePaymentObligation, token.amount], }); } // ... similar for ERC-721 and ERC-1155 tokens ``` ```rust // Example: Fulfilling ERC-20 payment demand let erc20_contract = contracts::IERC20::new(erc20_address, &wallet_provider); erc20_contract .approve(payment_obligation_address, amount) .send() .await? .get_receipt() .await?; let payment_contract = contracts::ERC20PaymentObligation::new( payment_obligation_address, &wallet_provider ); let payment_receipt = payment_contract .doObligation(contracts::ERC20PaymentObligation::ObligationData { token: erc20_address, amount: amount, payee: alice_address, }) .send() .await? .get_receipt() .await?; // Example: Fulfilling token bundle payment demand // Approve all tokens in the bundle first for erc20_token in &bundle_data.erc20s { let erc20_contract = contracts::IERC20::new(erc20_token.address, &wallet_provider); erc20_contract .approve(payment_obligation_address, erc20_token.value) .send() .await? .get_receipt() .await?; } // ... similar for ERC-721 and ERC-1155 tokens ``` ## Reclaiming expired escrow You can reclaim your escrow if nobody fulfills it before it expires. ```solidity // Alice reclaims her expired escrow erc20EscrowObligation.reclaimExpired(escrowUid); ``` ```typescript // Alice reclaims her expired escrow await walletClient.writeContract({ address: erc20EscrowObligation, abi: erc20EscrowAbi, functionName: "reclaimExpired", args: [escrowUid], }); ``` ```rust // Alice reclaims her expired escrow let escrow_contract = contracts::ERC20EscrowObligation::new( escrow_obligation_address, &wallet_provider ); escrow_contract .reclaimExpired(escrow_uid) .send() .await? .get_receipt() .await?; ``` ```bash # Alice reclaims her expired escrow alkahest --private-key 0xALICE_KEY escrow reclaim \ --erc20 --uid 0xESCROW_UID ``` ## Utility contracts and SDK functions There are utility contracts that provide a convenient interface for doing token trades atomically, and SDKs in TypeScript, Rust, and Python that wrap these. The SDKs additionally have functions that generate ERC-20 permits to enable easy approval and escrow/payment in one transaction. The functions to escrow one type of token, demanding any other type (buyYforX), and to fulfill any type of escrow demanding that token type (payXforY), are available in \[TokenType]BarterUtils.sol, or the corresponding barter module of each SDK (e.g. `client.erc20.barter`, `client.tokenBundle.barter`...). Available token types are native tokens (ETH), ERC-20, ERC-721, ERC-1155, and bundles of all of these together. For ERC-20 tokens, `permit_and_*` functions combine approval and action in a single gasless step. For escrow and payment modules, `approve_and_create` and `approve_and_pay` functions combine approval and action in two transactions but a single SDK call. ```solidity // Alice: Create escrow offering ERC-20 for ERC-721 using barter utils IERC20(usdcToken).approve(address(erc20BarterUtils), 1000e6); bytes32 escrowUid = erc20BarterUtils.buyErc721WithErc20( usdcToken, // bid token 1000e6, // bid amount erc721Token, // ask token 42, // ask token ID expiration ); // Bob: Fulfill escrow offering ERC-721 for ERC-20 IERC721(erc721Token).approve(address(erc721BarterUtils), 42); bytes32 paymentUid = erc721BarterUtils.payErc721ForErc20(escrowUid); // Or use permit for gasless approval (ERC-20 only) bytes32 escrowUid = erc20BarterUtils.permitAndBuyErc721WithErc20( usdcToken, 1000e6, erc721Token, 42, expiration, deadline, v, r, s // permit signature ); ``` ```typescript // Alice: Create escrow offering ERC-20 for ERC-721 (using permit for gasless approval) const { hash, attested } = await aliceClient.erc20.barter.permitAndBuyErc721WithErc20( { address: usdcToken, value: 1000000000n }, // bid { address: erc721Token, id: 42n }, // ask 0n, // no expiration ); // Bob: Approve and fulfill escrow offering ERC-721 for ERC-20 await bobClient.erc721.util.approve({ address: erc721Token, id: 42n }, "barter"); const payment = await bobClient.erc721.barter.payErc721ForErc20(attested.uid); // Can also use direct escrow/payment for custom demands (approveAndCreate combines both steps) const escrow = await aliceClient.erc20.escrow.nonTierable.approveAndCreate( { address: usdcToken, value: 1000000000n }, { arbiter: customArbiter, demand: customDemand }, 0n, ); ``` ```rust // Alice: Create escrow offering ERC-20 for ERC-721 (using permit for gasless approval) let bid = Erc20Data { address: usdc_token, value: U256::from(1000000000), }; let ask = Erc721Data { address: erc721_token, id: U256::from(42), }; let escrow_receipt = alice_client .erc20() .barter() .permit_and_buy_erc721_for_erc20(&bid, &ask, 0) .await?; let escrow_uid = DefaultAlkahestClient::get_attested_event(escrow_receipt)?.uid; // Bob: Approve and fulfill escrow offering ERC-721 for ERC-20 bob_client.erc721().approve(&ask, ApprovalPurpose::BarterUtils).await?; let payment_receipt = bob_client .erc721() .barter() .pay_erc721_for_erc20(escrow_uid) .await?; // Can also use direct escrow/payment for custom demands (approve_and_create combines both steps) let escrow_receipt = alice_client .erc20() .escrow() .non_tierable() .approve_and_create( &bid, &ArbiterData { arbiter: custom_arbiter, demand: custom_demand, }, 0, ) .await?; ``` ```python # Alice: Create escrow offering ERC-20 for ERC-721 (using permit for gasless approval) bid = {"address": usdc_token, "value": 1000000000} ask = {"address": erc721_token, "id": 42} escrow = await alice_client.erc20.barter.permit_and_buy_erc721_for_erc20(bid, ask, 0) # Bob: Approve and fulfill escrow offering ERC-721 for ERC-20 await bob_client.erc721.util.approve(ask, "barter") payment = await bob_client.erc721.barter.pay_erc721_for_erc20(escrow["log"]["uid"]) # Example of a complete ERC20-for-ERC20 trade using permit functions bid = {"address": usdc_token, "value": 1000} ask = {"address": eurc_token, "value": 900} # Alice creates escrow (permit handles approval) escrow = await alice_client.erc20.barter.permit_and_buy_erc20_for_erc20(bid, ask, 0) # Bob fulfills escrow (permit handles approval) payment = await bob_client.erc20.barter.permit_and_pay_erc20_for_erc20(escrow["log"]["uid"]) # Can also use direct escrow/payment for custom demands (approve_and_create combines both steps) escrow = await alice_client.erc20.escrow.non_tierable.approve_and_create( {"address": usdc_token, "value": 1000000000}, {"arbiter": custom_arbiter, "demand": custom_demand}, 0 ) ``` The CLI wraps the barter utility contracts. Use `--permit` for gasless ERC-20 approval: ```bash # Alice: Create barter (ERC20 for ERC20) with permit alkahest --private-key 0xALICE_KEY barter create \ --bid-type erc20 --ask-type erc20 \ --bid-token 0xUSDC --bid-amount 1000000000 \ --ask-token 0xEURC --ask-amount 900000000 \ --expiration 1735689600 \ --permit # Bob: Fulfill the barter alkahest --private-key 0xBOB_KEY barter fulfill \ --uid 0xBARTER_UID \ --bid-type erc20 --ask-type erc20 \ --permit # Or use escrow create with --approve for non-barter escrows alkahest --private-key 0xALICE_KEY escrow create \ --erc20 \ --token 0xUSDC --amount 1000000000 \ --arbiter 0xARBITER --demand 0xDEMAND \ --expiration 1735689600 \ --approve ``` --- ## Off-chain Oracles # Writing Arbiters (pt 2 - Off-chain Oracles) Off-chain oracles are arbitration services that run outside the blockchain but submit their decisions on-chain via the `TrustedOracleArbiter` contract. Unlike on-chain arbiters (see pt 1) that validate within smart contract execution, oracles perform arbitration in an external environment where they can access APIs, run complex computations, maintain state databases, or integrate with external systems. This guide covers implementing oracle services using the Alkahest SDKs. The patterns here apply to building production oracle infrastructure that can validate work submissions against arbitrary criteria. ## Understanding Your Role as an Oracle **Example scenario**: Alice wants Bob to capitalize a string ("hello world" → "HELLO WORLD"). She creates an escrow offering payment, specifying you (Charlie) as the trusted oracle who will verify if Bob's work is correct. Bob submits his result, requests your arbitration, and if you approve, he can claim the payment. Your oracle service needs to: 1. **Listen** for arbitration requests that specify you as the oracle 2. **Extract** the fulfillment data and original demand from attestations 3. **Validate** the fulfillment according to the demand criteria 4. **Submit** your approval or rejection decision on-chain The `TrustedOracleArbiter` contract handles the on-chain logic - your job is to implement the validation logic and submit decisions. For a complete example of how oracles fit into the escrow/fulfillment flow, see "Escrow Flow (pt 2 - Job Trading)". ## Three Validation Patterns import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; | Pattern | Returns | State | Escrow Access | Use Case | | ---------------- | ------- | ---------------------- | ------------------ | ------------------------------------------------------------ | | **Contextless** | bool | Oracle maintains state | No | Signature verification, identity validation, format checking | | **Demand-Based** | bool | Stateless | Yes - reads demand | Custom validation per escrow, test case validation | | **Asynchronous** | null | Job queue | Yes - reads demand | Time-based monitoring, long-running computations | **Decision flowchart:** - Does validation require waiting over time? - **Yes** → Asynchronous - **No** → Does validation need the escrow's demand parameters? - **Yes** → Demand-Based - **No** → Contextless ## Pattern 1: Contextless Validation Contextless oracles validate fulfillments based purely on the fulfillment's intrinsic properties and the oracle's own maintained state, without referencing the original escrow demand. This pattern is useful for building reusable validation services that work across any escrow. **When to use**: Signature verification, format checking, identity validation, standard verification against a maintained registry. **Why contextless**: The oracle provides a generic service (e.g., "I verify signatures from known identities") rather than validating against buyer-specific criteria. The validation logic doesn't depend on what Alice requested - only on what Bob submitted. **Composability**: Because contextless oracles are generic and reusable, they can be easily composed with other arbiters using logical combinators like `AllArbiter` and `AnyArbiter`. For example, you could require that a fulfillment is both signed by a verified identity (contextless oracle) AND meets specific job criteria (demand-based oracle). See "Escrow Flow (pt 3 - Composing Demands)" for composition patterns. **Reference implementations**: - Python: [`test_identity.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_identity.py) - TypeScript: [`offchain-oracle-identity.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-identity.test.ts) - Rust: [`offchain_oracle_identity.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_identity.rs) ### Step 1: Define fulfillment format and registry state Define what fulfillments look like and what state you maintain: ```python import json from dataclasses import dataclass from typing import Dict from eth_account import Account from eth_account.messages import encode_defunct @dataclass class IdentityFulfillment: pubkey: str nonce: int data: str signature: str # Oracle's internal registry (identity address -> current nonce) # This represents the oracle's concept of which identities are valid # and tracks nonces to prevent replay attacks identity_registry: Dict[str, int] = {} ``` ```typescript import { parseAbiParameters, recoverMessageAddress } from "viem"; // Fulfillment format (what sellers submit) type IdentityFulfillment = { pubkey: `0x${string}`; nonce: number; data: string; signature: `0x${string}`; }; // Oracle's internal registry (identity address -> current nonce) // This represents the oracle's concept of which identities are valid // and tracks nonces to prevent replay attacks const identityRegistry = new Map<`0x${string}`, number>(); // ABI for decoding StringObligation data const stringObligationAbi = parseAbiParameters("(string item)"); ``` ```rust use std::{collections::HashMap, sync::OnceLock}; use alloy::primitives::{Address, Signature, keccak256}; use tokio::sync::Mutex; // Fulfillment format (what sellers submit) #[derive(Debug, Clone, Serialize, Deserialize)] struct IdentityFulfillment { pubkey: Address, nonce: u64, data: String, signature: Vec, } // Oracle's internal registry (identity address -> current nonce) // This represents the oracle's concept of which identities are valid // and tracks nonces to prevent replay attacks static IDENTITY_REGISTRY: OnceLock>> = OnceLock::new(); fn identity_registry() -> &'static Mutex> { IDENTITY_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) } ``` ### Step 2: Initialize your registry Before starting the listener, populate your oracle's state: ```python async def run_contextless_oracle(oracle_client): # Register known identities with starting nonces identity_registry.clear() identity_registry[identity_address_1.lower()] = 0 identity_registry[identity_address_2.lower()] = 0 # In production: load from database # ... rest of oracle setup ... ``` ```typescript async function runContextlessOracle(charlieClient: AlkahestClient) { // Register known identities with starting nonces identityRegistry.set(identityAddress1, 0); identityRegistry.set(identityAddress2, 0); // In production: load from database // ... rest of oracle setup ... } ``` ```rust async fn run_contextless_oracle( charlie_client: AlkahestClient ) -> eyre::Result<()> { // Register known identities with starting nonces { let mut registry = identity_registry().lock().await; registry.insert(identity_address_1, 0); registry.insert(identity_address_2, 0); // In production: load from database } // ... rest of oracle setup ... } ``` ### Step 3: Implement the validation callback The validation callback receives the fulfillment attestation and the demand data, then checks it against your registry: ```python def verify_identity_decision(attestation, demand) -> bool: """ Verify an identity fulfillment by checking: 1. The signature is valid 2. The nonce is greater than the last seen nonce 3. The recovered address matches the claimed pubkey """ try: # Step 3a: Extract fulfillment data obligation_str = oracle_client.oracle.extract_obligation_data(attestation) payload = json.loads(obligation_str) parsed = IdentityFulfillment( pubkey=payload['pubkey'], nonce=payload['nonce'], data=payload['data'], signature=payload['signature'] ) # Step 3b: Check against oracle's registry pubkey_lower = parsed.pubkey.lower() if pubkey_lower not in identity_registry: return False # Unknown identity - not in our registry # Step 3c: Verify nonce progression (replay protection) current_nonce = identity_registry[pubkey_lower] if parsed.nonce <= current_nonce: return False # Step 3d: Verify signature format sig_len = len(parsed.signature) if parsed.signature else 0 if not parsed.signature or sig_len not in [130, 132]: return False # Step 3e: Verify cryptographic signature message = f"{parsed.data}:{parsed.nonce}" encoded_message = encode_defunct(text=message) try: recovered = Account.recover_message(encoded_message, signature=parsed.signature) except Exception: return False if recovered.lower() != pubkey_lower: return False # Signature mismatch # Step 3f: Update state and approve identity_registry[pubkey_lower] = parsed.nonce return True except Exception: return False ``` ```typescript async ({ attestation, demand }) => { // Step 3a: Extract fulfillment data const obligation = charlieClient.extractObligationData( stringObligationAbi, attestation, ); const payload = obligation[0]?.item; if (!payload) return false; // Step 3b: Parse the fulfillment payload let parsed: IdentityFulfillment; try { parsed = JSON.parse(payload) as IdentityFulfillment; } catch { return false; } // Step 3c: Check against oracle's registry const currentNonce = identityRegistry.get(parsed.pubkey); if (currentNonce === undefined) { return false; // Unknown identity - not in our registry } // Step 3d: Verify nonce progression (replay protection) if (parsed.nonce <= currentNonce) { return false; } // Step 3e: Verify signature format if (typeof parsed.signature !== "string" || parsed.signature.length !== 132) { return false; } // Step 3f: Verify cryptographic signature const message = `${parsed.data}:${parsed.nonce}`; let recovered: `0x${string}`; try { recovered = await recoverMessageAddress({ message, signature: parsed.signature, }); } catch { return false; } if (recovered.toLowerCase() !== parsed.pubkey.toLowerCase()) { return false; // Signature mismatch } // Step 3g: Update state and approve identityRegistry.set(parsed.pubkey, parsed.nonce); return true; } ``` ```rust fn verify_identity( awd: &alkahest_rs::clients::oracle::AttestationWithDemand, ) -> Pin> + Send>> { let attestation = awd.attestation.clone(); Box::pin(async move { // Step 3a: Extract fulfillment data let obligation: StringObligation::ObligationData = match StringObligation::ObligationData::abi_decode(&attestation.data) { Ok(o) => o, Err(_) => return Some(false), }; // Step 3b: Parse the fulfillment payload let payload = obligation.item.clone(); let parsed: IdentityFulfillment = match serde_json::from_str(&payload) { Ok(p) => p, Err(_) => return Some(false), }; // Step 3c: Check against oracle's registry let mut registry = identity_registry().lock().await; let Some(current_nonce) = registry.get_mut(&parsed.pubkey) else { return Some(false); // Unknown identity - not in our registry }; // Step 3d: Verify nonce progression (replay protection) if parsed.nonce <= *current_nonce { return Some(false); } // Step 3e: Verify cryptographic signature if parsed.signature.len() != 65 { return Some(false); } let sig = match Signature::try_from(parsed.signature.as_slice()) { Ok(s) => s, Err(_) => return Some(false), }; let message = format!("{}:{}", parsed.data, parsed.nonce); let hash = keccak256(message.as_bytes()); let Ok(recovered) = sig.recover_address_from_prehash(&hash) else { return Some(false); }; if recovered != parsed.pubkey { return Some(false); // Signature mismatch }; // Step 3f: Update state and approve *current_nonce = parsed.nonce; Some(true) }) } ``` ### Step 4: Set up listener and cleanup Wire everything together with the listener: ```python from alkahest_py import ArbitrationMode def callback(decision): # Optional: called after arbitration is submitted on-chain print(f"Arbitrated {decision.attestation.uid}: {decision.decision}") # Listen and validate decisions = await oracle_client.oracle.arbitrate_many( verify_identity_decision, callback, ArbitrationMode.AllUnarbitrated, timeout_seconds=10.0, ) # Cleanup identity_registry.clear() ``` ```typescript const listener = await charlieClient.oracle.listenAndArbitrate( // ... validation callback from Step 3 ... { skipAlreadyArbitrated: true }, ); // When shutting down the oracle listener.unwatch(); identityRegistry.clear(); ``` ```rust let listen_result = charlie_oracle .arbitrate_many_async( verify_identity, |decision| async move { // Optional: called after arbitration is submitted on-chain println!("Arbitrated {}: {}", decision.attestation.uid, decision.decision); }, ArbitrationMode::AllUnarbitrated, ) .await?; // Cleanup charlie_oracle.unsubscribe(listen_result.subscription_id).await?; ``` **Complete pattern:** 1. Define fulfillment format and registry state 2. Initialize your oracle's registry/database 3. Implement validation callback that: - Extracts fulfillment data - Checks against oracle's internal state - Performs validation (e.g., signature checks) - Updates state if needed - Returns `true` or `false` 4. Set up listener with callback and cleanup ## Pattern 2: Demand-Based Validation Demand-based oracles validate fulfillments against specific criteria provided by the buyer in the escrow demand. Each escrow can specify different requirements, and the oracle validates that Bob's fulfillment meets Alice's exact specifications. **When to use**: Custom validation criteria per escrow, need to compare fulfillment against buyer's specifications, computational validation with test cases. **Why demand-based**: Alice specifies exactly what she wants (e.g., "capitalize these specific test cases"), and the oracle verifies Bob's work matches those requirements. Different escrows have different demands, all validated by the same oracle. **Example flow**: 1. **Alice creates escrow** → demands `oracle=charlie`, `data="capitalize hello world"`, offers 100 tokens 2. **Bob fulfills** → submits `"HELLO WORLD"`, references Alice's escrow via `refUID` 3. **Bob requests arbitration** → asks Charlie to validate 4. **Charlie validates** → extracts Bob's result and Alice's query, checks if `"HELLO WORLD"` matches `uppercase("hello world")`, submits decision on-chain 5. **Bob claims payment** → uses approved attestation to collect escrow **Reference implementations**: - Python: [`test_capitalization.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_capitalization.py) - TypeScript: [`offchain-oracle-capitalization.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-capitalization.test.ts) - Rust: [`offchain_oracle_capitalization.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_capitalization.rs) ### Step 1: Define your demand format Decide what parameters buyers provide in their escrow demands: ```python import json import subprocess from dataclasses import dataclass from typing import List @dataclass class ShellTestCase: input: str output: str @dataclass class ShellOracleDemand: description: str test_cases: List[ShellTestCase] ``` ```typescript import { encodeAbiParameters, hexToBytes, parseAbiParameters, stringToHex, } from "viem"; import { exec as execCallback } from "node:child_process"; import { promisify } from "node:util"; const execAsync = promisify(execCallback); type ShellTestCase = { input: string; output: string; }; type ShellOracleDemand = { description: string; test_cases: ShellTestCase[]; }; // ABIs for decoding const stringObligationAbi = parseAbiParameters("(string item)"); const shellDemandAbi = parseAbiParameters("(bytes payload)"); ``` ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] struct TestCase { input: String, output: String, } #[derive(Debug, Clone, Serialize, Deserialize)] struct CommandTestDemand { description: String, test_cases: Vec, } ``` Buyers encode this as JSON in the `TrustedOracleArbiter` demand's `data` field. ### Step 2: Implement validation with demand The callback receives both the fulfillment attestation and the demand data directly. The demand is passed from the `ArbitrationRequested` event, so there's no need to fetch the escrow from chain. ```python async def decision_function(attestation, demand): """Evaluate whether the fulfillment meets the demand requirements""" try: # Step 2a: Extract fulfillment statement = oracle_client.oracle.extract_obligation_data(attestation) except Exception as e: print(f"Failed to extract obligation: {e}") return False # Step 2b: Parse demand from callback argument try: demand_json = json.loads(bytes(demand).decode('utf-8')) except Exception as e: print(f"Failed to parse demand: {e}") return False # Step 2c: Apply validation logic using demand parameters # Run each test case to verify Bob's submission works correctly for case in demand_json['test_cases']: command = f'echo "$INPUT" | {statement}' try: result = subprocess.run( ["bash", "-c", command], env={"INPUT": case['input']}, capture_output=True, text=True, timeout=5 ) if result.returncode != 0: return False output = result.stdout.rstrip('\n') if output != case['output']: return False except Exception: return False return True ``` ```typescript async ({ attestation, demand }) => { // Step 2a: Extract fulfillment const obligation = charlieClient.extractObligationData( stringObligationAbi, attestation, ); const statement = obligation[0]?.item; if (!statement) return false; // Step 2b: Parse demand from callback argument const outerDemand = charlieClient.arbiters.general.trustedOracle.decodeDemand(demand); const demandData = decodeAbiParameters(shellDemandAbi, outerDemand.data); const payloadHex = demandData[0]?.payload; if (!payloadHex) return false; let payload: ShellOracleDemand; try { const payloadJson = new TextDecoder().decode(hexToBytes(payloadHex)); payload = JSON.parse(payloadJson) as ShellOracleDemand; } catch { return false; } // Step 2c: Apply validation logic using demand parameters // Run each test case to verify Bob's submission works correctly for (const testCase of payload.test_cases) { const command = `echo "$INPUT" | ${statement}`; try { const { stdout } = await execAsync(command, { env: { ...process.env, INPUT: testCase.input, }, shell: "/bin/bash", }); if (stdout.trimEnd() !== testCase.output) { return false; } } catch { return false; } } return true; } ``` ```rust move |awd: &AttestationWithDemand| { let attestation = awd.attestation.clone(); let demand_bytes = awd.demand.clone(); let client = charlie_client_arc.clone(); async move { // Step 2a: Extract fulfillment let Ok(fulfillment) = client .extract_obligation_data::(&attestation) else { return Some(false); }; let submitted_command = fulfillment.item; // Step 2b: Parse demand from callback argument let Ok(test_demand) = serde_json::from_slice::( demand_bytes.as_ref() ) else { return Some(false); }; // Step 2c: Apply validation logic using demand parameters // Run each test case to verify Bob's submission works correctly for case in test_demand.test_cases { let full_command = format!("echo \"$INPUT\" | {}", submitted_command); let output = match Command::new("bash") .arg("-lc") .arg(&full_command) .env("INPUT", &case.input) .output() { Ok(output) if output.status.success() => { String::from_utf8_lossy(&output.stdout) .trim_end() .to_owned() } _ => return Some(false), }; if output != case.output { return Some(false); } } Some(true) } } ``` ### Step 3: Understanding the data flow ```text ArbitrationRequested Event └─ obligation: fulfillment UID └─ oracle: charlie_address └─ demand: bytes (your custom demand format) │ ▼ Callback receives (attestation, demand) │ ├─ attestation: Fulfillment Attestation │ └─ data: StringObligation { item: "tr '[:lower:]' '[:upper:]'" } │ └─ refUID: points to escrow │ └─ demand: bytes (passed directly from event) └─ Your custom format (e.g., JSON with test_cases) ``` The demand data is passed directly to your callback via the `ArbitrationRequested` event, so you don't need to fetch the escrow attestation from chain. ### Step 4: Set up the listener ```python from alkahest_py import ArbitrationMode def callback(decision): """Called when arbitration completes""" print(f"Arbitrated {decision.attestation.uid}: {decision.decision}") # Listen and arbitrate decisions = await oracle_client.oracle.arbitrate_many( decision_function, callback, ArbitrationMode.AllUnarbitrated, timeout_seconds=10.0, ) # Verify decisions assert all(d.decision for d in decisions), "Oracle rejected fulfillment" ``` ```typescript const listener = await charlieClient.arbiters.general.trustedOracle.arbitrateMany( // ... validation callback from Step 2 ... { mode: "allUnarbitrated" }, ); // When shutting down listener.unwatch(); ``` ```rust let listen_result = charlie_oracle .arbitrate_many_async( // ... validation callback from Step 2 ... |decision| async move { println!("Arbitrated {}: {}", decision.attestation.uid, decision.decision); }, &ArbitrationMode::AllUnarbitrated, ) .await?; charlie_oracle.unsubscribe(listen_result.subscription_id).await?; ``` **Complete pattern:** 1. Define demand format (your oracle's API) 2. Implement validation callback: - Extract fulfillment data from the attestation - Parse demand data from the callback argument - Apply validation logic comparing fulfillment to demand - Return `true` or `false` 3. Set up listener with callback ## Pattern 3: Asynchronous Validation Asynchronous oracles handle validation that cannot complete immediately. They require monitoring over time, accumulating data, or waiting for external conditions. The oracle schedules work for later, and a background worker submits the decision when ready. **When to use**: Time-based monitoring (uptime checks, deadline validation), accumulating evidence over multiple observations, long-running computations, waiting for consensus from multiple sources. **Why asynchronous**: Some validation is inherently time-based. For example, "verify this server stays up for 24 hours" cannot be validated instantly - you must schedule checks over time and make a final decision later. **Architecture**: ```text Listener (receives requests) → Job Queue (stores pending work) ↓ Worker (processes jobs over time) ↓ On-chain Submission (final decision) ``` **Reference implementations**: - Python: [`test_uptime.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_uptime.py) - TypeScript: [`offchain-oracle-uptime.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-uptime.test.ts) - Rust: [`offchain_oracle_uptime.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_uptime.rs) ### Step 1: Define demand format and job state Define both the demand buyers will provide and the internal state for tracking scheduled work: ```python import asyncio import json from dataclasses import dataclass, field from typing import Dict, List, Optional @dataclass class UptimeDemand: service_url: str min_uptime: float # Required uptime percentage (0.0-1.0) start: int # Start time for monitoring window end: int # End time for monitoring window check_interval_secs: int @dataclass class PingEvent: delay_secs: float success: bool @dataclass class UptimeJob: min_uptime: float schedule: List[PingEvent] demand: bytes # Shared state between listener and worker @dataclass class SchedulerContext: job_db: Dict[str, UptimeJob] = field(default_factory=dict) url_index: Dict[str, str] = field(default_factory=dict) notify: Optional[asyncio.Event] = field(default_factory=asyncio.Event) ``` ```typescript import { parseAbiParameters, stringToHex, encodeAbiParameters, hexToBytes } from "viem"; // Demand format (buyers provide this) type UptimeDemand = { service_url: string; min_uptime: number; // Required uptime percentage (0.0-1.0) start: number; // Start time for monitoring window end: number; // End time for monitoring window check_interval_secs: number; }; // Internal job state (your oracle tracks this) type PingEvent = { delayMs: number; success: boolean; }; type UptimeJob = { minUptime: number; schedule: PingEvent[]; demandData: `0x${string}`; }; // Shared state between listener and worker type SchedulerContext = { jobDb: Map<`0x${string}`, UptimeJob>; urlIndex: Map; waiters: Array<() => void>; }; let schedulerContext: SchedulerContext | undefined; // ABIs for decoding const stringObligationAbi = parseAbiParameters("(string item)"); const uptimeDemandAbi = parseAbiParameters("(bytes payload)"); ``` ```rust use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use tokio::sync::{Mutex, Notify}; // Demand format (buyers provide this) #[derive(Debug, Clone, Serialize, Deserialize)] struct UptimeDemand { service_url: String, min_uptime: f64, // Required uptime percentage (0.0-1.0) start: u64, // Start time for monitoring window end: u64, // End time for monitoring window check_interval_secs: u64, } // Internal job state (your oracle tracks this) #[derive(Debug, Clone)] struct PingEvent { delay: StdDuration, success: bool, } #[derive(Debug, Clone)] struct UptimeJob { min_uptime: f64, schedule: Vec, demand: Bytes, } // Shared state between listener and worker type JobDb = Arc, UptimeJob>>>; #[derive(Clone)] struct SchedulerContext { job_db: JobDb, notify: Arc, url_index: Arc>>>, client: Arc>, } static SCHEDULER_STATE: OnceLock>> = OnceLock::new(); ``` ### Step 2: Implement the scheduling callback The listener callback schedules work but **does not make a decision** — it returns `None` (`null`) to defer the decision to the worker: ```python async def schedule_decision(attestation, demand): """Schedule uptime monitoring but DON'T make a decision yet.""" try: # Step 2a: Extract service URL from fulfillment statement = oracle_client.oracle.extract_obligation_data(attestation) # Look up the fulfillment UID from our URL index uid = ctx.url_index.get(statement) if uid is None or uid in ctx.job_db: return None # Step 2b: Parse demand from callback argument demand_json = json.loads(bytes(demand).decode("utf-8")) # Verify URL matches if statement != demand_json["service_url"]: return None # Step 2c: Create monitoring schedule total_span = max(demand_json["end"] - demand_json["start"], 1) interval = max(demand_json["check_interval_secs"], 1) checks = max(total_span // interval, 1) schedule = [] for i in range(checks): schedule.append(PingEvent( delay_secs=0.1 + i * 0.025, success=True, # Will be determined by actual ping )) # Step 2d: Store job for background processing ctx.job_db[uid] = UptimeJob( min_uptime=demand_json["min_uptime"], schedule=schedule, demand=bytes(demand), ) ctx.notify.set() # Wake up worker # Step 2e: Return None to defer decision to the worker return None except Exception: return None ``` ```typescript async ({ attestation, demand }) => { const ctx = getScheduler(); if (!ctx) return null; // Step 2a: Extract service URL from fulfillment const obligation = charlieClient.extractObligationData( stringObligationAbi, attestation, ); const statement = obligation[0]; if (!statement?.item) return null; const fulfillmentUid = ctx.urlIndex.get(statement.item); if (!fulfillmentUid || ctx.jobDb.has(fulfillmentUid)) return null; // Step 2b: Parse demand from callback argument const outerDemand = charlieClient.arbiters.general.trustedOracle.decodeDemand(demand); const demandData = decodeAbiParameters(uptimeDemandAbi, outerDemand.data); const payloadHex = demandData[0]?.payload; if (!payloadHex) return null; let parsed: UptimeDemand; try { const json = new TextDecoder().decode(hexToBytes(payloadHex)); parsed = JSON.parse(json) as UptimeDemand; } catch { return null; } // Step 2c: Create monitoring schedule const totalSpan = Math.max(parsed.end - parsed.start, 1); const interval = Math.max(parsed.check_interval_secs, 1); const checks = Math.max(Math.floor(totalSpan / interval), 1); const schedule: PingEvent[] = []; for (let i = 0; i < checks; i++) { schedule.push({ delayMs: 100 + i * 25, success: true }); } // Step 2d: Store job for background processing const rawDemandBytes = encodeAbiParameters(uptimeDemandAbi, demandData) as `0x${string}`; ctx.jobDb.set(fulfillmentUid, { minUptime: parsed.min_uptime, schedule, demandData: rawDemandBytes, }); notifyScheduler(ctx); // Wake up worker // Step 2e: Return null to defer decision to the worker return null; } ``` ```rust fn schedule_pings( awd: &alkahest_rs::clients::oracle::AttestationWithDemand, ) -> Pin> + Send>> { let attestation = awd.attestation.clone(); let demand_bytes = awd.demand.clone(); Box::pin(async move { let ctx_opt = scheduler_state().lock().await.clone(); let Some(ctx) = ctx_opt else { return None; }; // Step 2a: Extract service URL from fulfillment let Ok(statement) = ctx.client .extract_obligation_data::(&attestation) else { return None; }; let url = statement.item.clone(); let Some(uid) = ctx.url_index.lock().await.get(&url).cloned() else { return None; }; // Step 2b: Parse demand from callback argument let Ok(parsed_demand) = serde_json::from_slice::( demand_bytes.as_ref() ) else { return None; }; // Step 2c: Create monitoring schedule let total_span = parsed_demand.end.saturating_sub(parsed_demand.start).max(1); let interval = parsed_demand.check_interval_secs.max(1); let checks = (total_span / interval).max(1) as usize; let mut schedule = Vec::with_capacity(checks); for i in 0..checks { schedule.push(PingEvent { delay: StdDuration::from_millis(100 + (i * 25) as u64), success: true, // Will be determined by actual ping }); } // Step 2d: Store job for background processing ctx.job_db.lock().await.entry(uid).or_insert(UptimeJob { min_uptime: parsed_demand.min_uptime, schedule, demand: demand_bytes, }); ctx.notify.notify_one(); // Wake up worker // Step 2e: Return None to defer decision to the worker None }) } ``` ### Step 3: Implement the background worker Create a worker that processes scheduled jobs and submits decisions: ```python async def run_worker(ctx: SchedulerContext, oracle_client) -> None: """Background worker that processes uptime jobs and submits decisions.""" while True: # Step 3a: Get next job from queue uid = None job = None for k, v in list(ctx.job_db.items()): uid = k job = v del ctx.job_db[k] break if uid is not None and job is not None: # Step 3b: Execute the monitoring schedule successes = 0 total_checks = max(len(job.schedule), 1) for ping in job.schedule: await asyncio.sleep(ping.delay_secs) # In production: actually ping the service if ping.success: successes += 1 # Step 3c: Calculate result and make decision uptime = successes / total_checks decision = uptime >= job.min_uptime # Step 3d: Submit decision on-chain # CRITICAL: Unlike synchronous oracles where arbitrate_many # handles submission automatically, async oracles must MANUALLY # call oracle.arbitrate() to submit their decision await oracle_client.oracle.arbitrate(uid, list(job.demand), decision) else: # Step 3e: Wait for new work ctx.notify.clear() try: await asyncio.wait_for(ctx.notify.wait(), timeout=2.0) except asyncio.TimeoutError: if not ctx.job_db: break # No work and no notifications - exit ``` ```typescript function startSchedulerWorker( ctx: SchedulerContext, arbiters: typeof charlieClient.arbiters, ) { let active = true; const promise = (async () => { while (active) { // Step 3a: Get next job from queue let entry = dequeueJob(ctx); if (!entry) { const hasJob = await waitForJob(ctx, 2000); if (!hasJob) { if (ctx.jobDb.size === 0) break; continue; } entry = dequeueJob(ctx); if (!entry) continue; } const { uid, job } = entry; // Step 3b: Execute the monitoring schedule let successes = 0; const totalChecks = Math.max(job.schedule.length, 1); for (const ping of job.schedule) { await Bun.sleep(ping.delayMs); // In production: actually ping the service if (ping.success) successes += 1; } // Step 3c: Calculate result and make decision const uptime = successes / totalChecks; const decision = uptime >= job.minUptime; // Step 3d: Submit decision on-chain // CRITICAL: Unlike synchronous oracles where listenAndArbitrate // handles submission automatically, async oracles must MANUALLY // call arbitrate() to submit their decision await arbiters.general.trustedOracle.arbitrate(uid, job.demandData, decision); } })(); return { stop: () => { active = false; notifyScheduler(ctx); }, promise, }; } ``` ```rust async fn run_worker( job_db: JobDb, notify: Arc, arbiters: ArbitersModule, ) { loop { // Step 3a: Get next job from queue let maybe_job = { let mut db = job_db.lock().await; if let Some((&uid, job)) = db.iter().next() { let job = job.clone(); db.remove(&uid); Some((uid, job)) } else { None } }; if let Some((uid, job)) = maybe_job { // Step 3b: Execute the monitoring schedule let mut successes = 0usize; let total_checks = job.schedule.len().max(1); for ping in job.schedule { tokio::time::sleep(ping.delay).await; // In production: actually ping the service if ping.success { successes += 1; } } // Step 3c: Calculate result and make decision let uptime = successes as f64 / total_checks as f64; let decision = uptime >= job.min_uptime; // Step 3d: Submit decision on-chain // CRITICAL: Unlike synchronous oracles where arbitrate_many_async // handles submission automatically, async oracles must MANUALLY // call arbitrate() to submit their decision arbiters .trusted_oracle() .arbitrate(uid, job.demand.clone(), decision) .await .expect("oracle arbitration tx should succeed"); } else { // Step 3e: Wait for new work match tokio::time::timeout(StdDuration::from_secs(2), notify.notified()).await { Ok(_) => continue, Err(_) => { if job_db.lock().await.is_empty() { break; // No work and no notifications - exit } } } } } } ``` ### Step 4: Wire everything together Set up the listener, worker, and shared state: ```python from alkahest_py import ArbitrationMode # Step 4a: Initialize shared state ctx = SchedulerContext() ctx.url_index[service_url] = fulfillment_uid # Step 4b: Start background worker worker_task = asyncio.create_task(run_worker(ctx, oracle_client)) # Step 4c: Start listener (scheduling callback from Step 2) decisions = await oracle_client.oracle.arbitrate_many( schedule_decision, callback, ArbitrationMode.AllUnarbitrated, timeout_seconds=10.0, ) # Step 4d: Wait for worker and cleanup await asyncio.wait_for(worker_task, timeout=10.0) ``` ```typescript // Step 4a: Initialize shared state const scheduler: SchedulerContext = { jobDb: new Map(), urlIndex: new Map(), waiters: [], }; scheduler.urlIndex.set(serviceUrl, fulfillmentUid); setScheduler(scheduler); // Step 4b: Start background worker const worker = startSchedulerWorker(scheduler, charlieClient.arbiters); // Step 4c: Start listener (scheduling callback from Step 2) const listener = await charlieClient.arbiters.general.trustedOracle.arbitrateMany( // ... scheduling callback ... { mode: "allUnarbitrated" }, ); // Step 4d: Wait and cleanup listener.unwatch(); worker.stop(); await worker.promise; setScheduler(undefined); ``` ```rust // Step 4a: Initialize shared state let job_db: JobDb = Arc::new(Mutex::new(HashMap::new())); let scheduler_notify = Arc::new(Notify::new()); { let mut slot = scheduler_state().lock().await; *slot = Some(SchedulerContext { job_db: Arc::clone(&job_db), notify: Arc::clone(&scheduler_notify), url_index: Arc::clone(&url_index), client: Arc::new(charlie_client.clone()), }); } // Step 4b: Start background worker let worker = tokio::spawn(async move { run_worker(worker_db, worker_notify, worker_arbiters).await; }); // Step 4c: Start listener (scheduling callback from Step 2) let listen_result = charlie_oracle .arbitrate_many_async( schedule_pings, |_| async {}, ArbitrationMode::AllUnarbitrated, ) .await?; // Step 4d: Wait and cleanup worker.await.unwrap(); charlie_oracle.unsubscribe(listen_result.subscription_id).await?; { let mut slot = scheduler_state().lock().await; *slot = None; } ``` **Complete asynchronous oracle pattern:** 1. Define demand format and job state structures 2. Implement scheduling callback that: - Extracts fulfillment data - Parses demand from callback argument - Creates job schedule - Stores job in shared queue - Returns `None`/`null` (defers decision) 3. Implement background worker that: - Polls job queue - Executes scheduled work - Makes decision based on results - Calls `arbitrate()` to submit decision on-chain 4. Wire together with shared state and cleanup **Key differences from synchronous patterns:** | Aspect | Synchronous (Patterns 1 & 2) | Asynchronous (Pattern 3) | | --------------------- | ----------------------------- | ------------------------------------------ | | Callback return value | `true` or `false` | `None`/`null` (defers decision) | | Decision submission | Automatic via SDK | Manual via `arbitrate()` | | Architecture | Single callback function | Callback + background worker | | State management | Optional (Pattern 1 only) | Required (job queue) | | Timing | Instant validation | Delayed validation over time | ## Choosing the Right Pattern **Quick decision tree:** 1. Does validation require waiting/monitoring over time? - **YES** → Pattern 3: Asynchronous 2. Does validation need the escrow's demand parameters? - **YES** → Pattern 2: Demand-Based - **NO** → Pattern 1: Contextless **Detailed comparison:** | Validation Type | Pattern | Complexity | Example | | -------------------------- | ------------ | ---------- | ---------------------------- | | Signature verification | Contextless | Low | Verify identity attestations | | Format/standard checking | Contextless | Low | Validate JSON schemas | | Test case validation | Demand-Based | Medium | Run buyer-specified tests | | Computational verification | Demand-Based | Medium | Check algorithmic solutions | | Uptime monitoring | Asynchronous | High | Verify 99% uptime over 24h | | Consensus voting | Asynchronous | High | Wait for multiple approvals | ## Production Considerations ### Error Handling and Logging Always handle errors gracefully and log for debugging: ```python import logging logger = logging.getLogger(__name__) # In validation callback try: data = parse_data(attestation) except Exception as e: logger.error(f"Failed to parse attestation {attestation.uid}: {e}") return False # Reject invalid data # Log decisions logger.info( f"Oracle decision for {attestation.uid}: " f"{'approved' if decision else 'rejected'}" ) ``` ```typescript // In validation callback try { const data = await parseData(attestation); } catch (error) { console.error("Failed to parse attestation:", attestation.uid, error); return false; // Reject invalid data } // Log decisions console.log( `Oracle decision for ${attestation.uid}: ${decision ? "approved" : "rejected"}` ); ``` ```rust use tracing::{info, warn, error}; // In validation callback let Ok(data) = parse_data(&attestation) else { error!("Failed to parse attestation: {:?}", attestation.uid); return Some(false); // Reject invalid data }; // Log decisions info!( uid = ?attestation.uid, decision = decision, "Oracle decision: {}", if decision { "approved" } else { "rejected" } ); ``` ### State Persistence For async oracles, persist state to survive restarts: ```python import sqlite3 import json class PersistentJobDb: def __init__(self, db_path: str): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE IF NOT EXISTS jobs ( uid TEXT PRIMARY KEY, job TEXT NOT NULL ) """) self.conn.commit() def insert_job(self, uid: str, job: UptimeJob): job_json = json.dumps({ 'fulfillment_uid': job.fulfillment_uid, 'service_url': job.service_url, 'min_uptime': job.min_uptime, 'checks_remaining': job.checks_remaining, 'successes': job.successes, 'total_checks': job.total_checks, }) self.conn.execute( "INSERT OR REPLACE INTO jobs (uid, job) VALUES (?, ?)", (uid, job_json) ) self.conn.commit() def get_next_job(self): cursor = self.conn.execute("SELECT uid, job FROM jobs LIMIT 1") row = cursor.fetchone() if not row: return None uid, job_json = row job_data = json.loads(job_json) job = UptimeJob(**job_data) self.conn.execute("DELETE FROM jobs WHERE uid = ?", (uid,)) self.conn.commit() return (uid, job) ``` ```typescript import Database from "better-sqlite3"; class PersistentJobDb { private db: Database.Database; constructor(filepath: string) { this.db = new Database(filepath); this.db.exec(` CREATE TABLE IF NOT EXISTS jobs ( uid TEXT PRIMARY KEY, job TEXT NOT NULL ) `); } insertJob(uid: `0x${string}`, job: UptimeJob) { const stmt = this.db.prepare("INSERT OR REPLACE INTO jobs (uid, job) VALUES (?, ?)"); stmt.run(uid, JSON.stringify(job)); } getNextJob(): { uid: `0x${string}`; job: UptimeJob } | undefined { const row = this.db.prepare("SELECT uid, job FROM jobs LIMIT 1").get() as | { uid: string; job: string } | undefined; if (!row) return undefined; this.db.prepare("DELETE FROM jobs WHERE uid = ?").run(row.uid); return { uid: row.uid as `0x${string}`, job: JSON.parse(row.job) as UptimeJob, }; } } ``` ```rust // Use sled, rocksdb, or postgres for production use sled::Db; struct PersistentJobDb { db: Db, } impl PersistentJobDb { fn insert_job(&self, uid: &FixedBytes<32>, job: &UptimeJob) -> eyre::Result<()> { let value = bincode::serialize(job)?; self.db.insert(uid.as_slice(), value)?; Ok(()) } fn get_next_job(&self) -> eyre::Result, UptimeJob)>> { if let Some(Ok((key, value))) = self.db.iter().next() { let uid = FixedBytes::<32>::from_slice(&key); let job: UptimeJob = bincode::deserialize(&value)?; self.db.remove(&key)?; Ok(Some((uid, job))) } else { Ok(None) } } } ``` ### Retry Logic Handle transaction failures gracefully: ```python import asyncio async def submit_with_retry( oracle_client, uid: str, demand: bytes, decision: bool, max_attempts: int = 3 ): for attempt in range(max_attempts): try: await oracle_client.oracle.arbitrate(uid, list(demand), decision) return except Exception as e: logger.warning(f"Arbitration attempt {attempt + 1} failed: {e}") if attempt < max_attempts - 1: await asyncio.sleep(2 ** attempt) raise Exception(f"Failed to submit arbitration after {max_attempts} attempts") ``` ```typescript async function submitWithRetry( arbiters: typeof charlieClient.arbiters, uid: `0x${string}`, demandData: `0x${string}`, decision: boolean, maxAttempts = 3, ) { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { await arbiters.general.trustedOracle.arbitrate(uid, demandData, decision); return; } catch (error) { console.warn(`Arbitration attempt ${attempt + 1} failed:`, error); if (attempt < maxAttempts - 1) { await Bun.sleep(2 ** attempt * 1000); } } } throw new Error(`Failed to submit arbitration after ${maxAttempts} attempts`); } ``` ```rust async fn submit_with_retry( arbiters: &ArbitersModule, uid: FixedBytes<32>, demand: Bytes, decision: bool, ) -> eyre::Result<()> { for attempt in 0..3 { match arbiters.trusted_oracle().arbitrate(uid, demand.clone(), decision).await { Ok(_) => return Ok(()), Err(e) => { warn!("Arbitration attempt {} failed: {}", attempt + 1, e); tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await; } } } Err(eyre!("Failed to submit arbitration after 3 attempts")) } ``` ## Reference Implementations See the full working examples in the test suites: - **Pattern 1 (Contextless)**: Identity verification with signature validation - Python: [`test_identity.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_identity.py) - TypeScript: [`offchain-oracle-identity.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-identity.test.ts) - Rust: [`offchain_oracle_identity.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_identity.rs) - **Pattern 2 (Demand-Based)**: Test case validation for shell commands - Python: [`test_capitalization.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_capitalization.py) - TypeScript: [`offchain-oracle-capitalization.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-capitalization.test.ts) - Rust: [`offchain_oracle_capitalization.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_capitalization.rs) - **Pattern 3 (Asynchronous)**: Uptime monitoring with background worker - Python: [`test_uptime.py`](https://github.com/arkhai-io/alkahest/blob/main/sdks/py/alkahest_py/oracle/test_uptime.py) - TypeScript: [`offchain-oracle-uptime.test.ts`](https://github.com/arkhai-io/alkahest/blob/main/sdks/ts/tests/integration/offchain-oracle-uptime.test.ts) - Rust: [`offchain_oracle_uptime.rs`](https://github.com/arkhai-io/alkahest/blob/main/sdks/rs/tests/offchain_oracle_uptime.rs) These tests demonstrate the complete flow including escrow creation (Alice), fulfillment submission (Bob), oracle validation (Charlie), and payment collection. --- ## On-chain Arbiters import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Writing Arbiters (pt 1 - On-chain Arbiters) Arbiters are smart contracts that implement the `IArbiter` interface to validate whether an obligation (attestation) satisfies specific requirements. They act as on-chain judges that determine if conditions have been met for releasing escrowed assets or fulfilling other blockchain operations. ## Single-transaction arbiters Single-transaction arbiters perform validation synchronously within a single transaction. They check if an obligation meets the specified demands immediately when called. ### Example 1: String Capitalizer - Synchronous On-chain Computation This example demonstrates how to write an arbiter that performs synchronous on-chain computations to validate data transformations. It works with a generic data-holding obligation format (`StringObligation`). **Pattern illustrated**: Validating computational results where both the input (demand) and output (obligation) are stored on-chain, and the validation logic can be executed deterministically. ```solidity contract StringCapitalizer is IArbiter { using ArbiterUtils for Attestation; struct DemandData { string query; // Input data } function checkObligation( Attestation memory obligation, bytes memory demand, bytes32 counteroffer ) external view override returns (bool) { // Step 1: Validate attestation integrity if (!obligation._checkIntrinsic()) return false; // Step 2: Check counteroffer reference if provided if (counteroffer != bytes32(0) && obligation.refUID != counteroffer) { return false; } // Step 3: Decode both demand and obligation data DemandData memory demandData = abi.decode(demand, (DemandData)); StringObligation.ObligationData memory obligationData = abi.decode(obligation.data, (StringObligation.ObligationData)); // Step 4: Apply validation logic return _isCapitalized(demandData.query, obligationData.item); } function _isCapitalized( string memory query, string memory result ) internal pure returns (bool) { // Deterministic computation to validate transformation bytes memory queryBytes = bytes(query); bytes memory resultBytes = bytes(result); if (queryBytes.length != resultBytes.length) return false; for (uint256 i = 0; i < queryBytes.length; i++) { uint8 queryChar = uint8(queryBytes[i]); uint8 resultChar = uint8(resultBytes[i]); if (queryChar >= 0x61 && queryChar <= 0x7A) { // Lowercase should be capitalized if (resultChar != queryChar - 32) return false; } else { // Non-lowercase should remain unchanged if (resultChar != queryChar) return false; } } return true; } } ``` **When to use this pattern**: - Validating mathematical computations - Checking data transformations (encoding, formatting, etc.) - Verifying algorithmic solutions - Any scenario where validation logic is purely computational ### Example 2: Game Winner - Conditional Escrow for External Attestations This example demonstrates how to write an arbiter that creates a conditional escrow system for EAS attestations originating from external sources (like a game contract). **Pattern illustrated**: Validating attestations from trusted external systems, where the arbiter acts as a bridge between existing attestation infrastructure and escrow mechanisms. ```solidity contract GameWinner is IArbiter { using ArbiterUtils for Attestation; IEAS public immutable eas; bytes32 public immutable GAME_WINNER_SCHEMA; address public immutable trustedGameContract; struct GameWinnerData { bytes32 gameId; address winner; uint256 timestamp; uint256 score; } struct ClaimDemand { bytes32 gameId; uint256 minScore; uint256 validAfter; } function checkObligation( Attestation memory obligation, bytes memory demand, bytes32 counteroffer ) external view override returns (bool) { // Step 1: Validate attestation integrity if (!obligation._checkIntrinsic()) return false; // Step 2: Verify attestation source and type if (obligation.schema != GAME_WINNER_SCHEMA) return false; if (obligation.attester != trustedGameContract) return false; // Step 3: Check counteroffer reference if provided if (counteroffer != bytes32(0) && obligation.refUID != counteroffer) { return false; } // Step 4: Decode and validate against demand criteria GameWinnerData memory winnerData = abi.decode(obligation.data, (GameWinnerData)); ClaimDemand memory claimDemand = abi.decode(demand, (ClaimDemand)); // Step 5: Apply conditional logic if (winnerData.gameId != claimDemand.gameId) return false; if (winnerData.score < claimDemand.minScore) return false; if (winnerData.timestamp < claimDemand.validAfter) return false; // Step 6: Verify attestation ownership if (obligation.recipient != winnerData.winner) return false; return true; } } ``` **When to use this pattern**: - Bridging external attestation systems with escrow contracts - Creating conditional release mechanisms based on third-party validations - Implementing trust-based validation where specific attesters are authorized - Building on top of existing EAS infrastructure ## Common Implementation Pattern All single-transaction arbiters follow this structure: 1. **Define data structures**: Create structs for demand parameters and expected obligation data 2. **Implement `checkObligation`**: The main validation function that returns a boolean 3. **Validate attestation integrity**: Use `ArbiterUtils` to check expiration and revocation 4. **Verify schema and source** (if needed): Ensure attestations match expected format and origin 5. **Decode data**: Extract information from both obligation and demand 6. **Apply validation logic**: Implement the specific rules for your use case 7. **Return verdict**: Simple pass/fail boolean The key difference between examples: - **Computational validation** (StringCapitalizer): Demonstrates algorithmic verification of data transformations - **Attestation bridging** (GameWinner): Demonstrates verification of external attestations with conditional logic ## Asynchronous arbiters Asynchronous arbiters handle validation that cannot be completed in a single transaction. They maintain internal state across multiple transactions and only return true from `checkObligation` once a multi-step process has been completed. ### Example 3: MajorityVoteArbiter - Multi-transaction State Accumulation This example demonstrates how to write an arbiter that accumulates state over multiple transactions before making a validation decision. It uses voting as an example of a process that requires multiple interactions before completion. **Pattern illustrated**: Multi-transaction validation where the arbiter stores intermediate state and only approves obligations after a defined process reaches completion. The validation logic spans multiple transactions with state persistence between them. ```solidity contract MajorityVoteArbiter is IArbiter { using ArbiterUtils for Attestation; struct DemandData { address[] voters; // List of authorized voters uint256 quorum; // Minimum votes required for approval bytes data; // Additional context for voters } struct VoteStatus { uint256 yesVotes; uint256 noVotes; mapping(address => bool) hasVoted; mapping(address => bool) vote; } IEAS public immutable eas; mapping(bytes32 => VoteStatus) public voteStatuses; // Events for transparency event VoteCast( bytes32 indexed obligation, address indexed voter, bool vote, uint256 yesVotes, uint256 noVotes ); event ArbitrationComplete( bytes32 indexed obligation, bool decision, uint256 yesVotes, uint256 noVotes ); function castVote( bytes32 obligation, bool vote, bytes calldata demandData ) external { DemandData memory demand = abi.decode(demandData, (DemandData)); // Verify voter authorization bool isAuthorized = false; for (uint256 i = 0; i < demand.voters.length; i++) { if (demand.voters[i] == msg.sender) { isAuthorized = true; break; } } if (!isAuthorized) revert UnauthorizedVoter(); VoteStatus storage status = voteStatuses[obligation]; // Prevent double voting if (status.hasVoted[msg.sender]) revert AlreadyVoted(); // Record vote status.hasVoted[msg.sender] = true; status.vote[msg.sender] = vote; if (vote) { status.yesVotes++; } else { status.noVotes++; } emit VoteCast(obligation, msg.sender, vote, status.yesVotes, status.noVotes); // Check if decision is reached if (status.yesVotes >= demand.quorum) { emit ArbitrationComplete(obligation, true, status.yesVotes, status.noVotes); } else if (status.noVotes > demand.voters.length - demand.quorum) { // Quorum becomes impossible emit ArbitrationComplete(obligation, false, status.yesVotes, status.noVotes); } } function checkObligation( Attestation memory obligation, bytes memory demand, bytes32 /* counteroffer */ ) public view override returns (bool) { // Validate attestation integrity if (!obligation._checkIntrinsic()) return false; DemandData memory demandData = abi.decode(demand, (DemandData)); VoteStatus storage status = voteStatuses[obligation.uid]; // Return true if quorum reached with yes votes return status.yesVotes >= demandData.quorum; } } ``` **Key Implementation Aspects**: 1. **State Storage**: The contract maintains a mapping (`voteStatuses`) that persists state between transactions 2. **Process Accumulation**: Multiple transactions (`castVote`) modify the stored state incrementally 3. **Completion Detection**: The arbiter tracks when the process reaches a deterministic outcome 4. **Deferred Validation**: `checkObligation` returns false until the process completes, then returns the final decision **When to use this pattern**: - Validation that requires waiting for external events or timeouts - Processes that need multiple parties to submit data before validation - Scenarios where validation depends on accumulating evidence over time - Any validation that cannot be deterministically computed in a single transaction **Pattern Variations**: - **Time-based**: Wait for a specific time period before allowing validation - **Oracle-based**: Wait for external data submission from trusted sources - **Challenge-based**: Allow a challenge period before finalizing validation - **Threshold-based**: Accumulate metrics/scores until a threshold is reached **Alternative Implementation Note**: Rather than implementing complex multi-transaction logic directly in an arbiter, you could also create a separate contract that manages the multi-step process and then submits a final result to `TrustedOracleArbiter`. This separation of concerns can make the system more modular and reusable. ## Implementing SDK extensions When writing custom arbiters, you'll want to create corresponding SDK extensions to make them easy to use from client applications. The Alkahest SDKs (TypeScript, Rust, and Python) use a hierarchical module architecture. ### TypeScript SDK Extensions The TypeScript SDK uses a factory pattern with hierarchical namespacing. To add support for a new arbiter: 1. **Create the arbiter module** with static encode/decode functions and a client factory: ```typescript // alkahest-ts/src/clients/arbiters/custom/majorityVote.ts import { decodeAbiParameters, encodeAbiParameters, getAbiItem, parseAbiItem } from "viem"; import { abi as majorityVoteArbiterAbi } from "../../../contracts/arbiters/MajorityVoteArbiter"; import type { ChainAddresses } from "../../../types"; import type { ViemClient } from "../../../utils"; // Extract DemandData type from contract ABI const decodeDemandFunction = getAbiItem({ abi: majorityVoteArbiterAbi.abi, name: "decodeDemandData", }); const demandDataType = decodeDemandFunction.outputs[0]; export type MajorityVoteDemandData = { voters: `0x${string}`[]; quorum: bigint; data: `0x${string}`; }; // Static encode/decode functions (usable without client) export const encodeDemand = (demand: MajorityVoteDemandData): `0x${string}` => { return encodeAbiParameters([demandDataType], [demand]); }; export const decodeDemand = (demandData: `0x${string}`): MajorityVoteDemandData => { return decodeAbiParameters([demandDataType], demandData)[0] as MajorityVoteDemandData; }; // Client factory for instance methods export const makeMajorityVoteClient = (viemClient: ViemClient, addresses: ChainAddresses) => { const voteEvent = parseAbiItem( "event VoteCast(bytes32 indexed obligation, address indexed voter, bool vote, uint256 yesVotes, uint256 noVotes)" ); return { encodeDemand, decodeDemand, castVote: async (obligation: `0x${string}`, vote: boolean, demandData: `0x${string}`) => { return await viemClient.writeContract({ address: addresses.majorityVoteArbiter, abi: majorityVoteArbiterAbi.abi, functionName: "castVote", args: [obligation, vote, demandData], }); }, getVoteStatus: async (obligation: `0x${string}`) => { const status = await viemClient.readContract({ address: addresses.majorityVoteArbiter, abi: majorityVoteArbiterAbi.abi, functionName: "voteStatuses", args: [obligation], }); return { yesVotes: status[0], noVotes: status[1] }; }, waitForQuorum: async (obligation: `0x${string}`, quorum: bigint) => { // Watch for VoteCast events until quorum reached return new Promise((resolve) => { const unwatch = viemClient.watchEvent({ address: addresses.majorityVoteArbiter, event: voteEvent, args: { obligation }, onLogs: (logs) => { const latest = logs[logs.length - 1]; if (latest && BigInt(latest.args.yesVotes!) >= quorum) { unwatch(); resolve(latest.args); } }, }); }); }, }; }; ``` 2. **Integrate into the hierarchical client** by adding to a parent module: ```typescript // alkahest-ts/src/clients/arbiters/custom/index.ts import type { ChainAddresses } from "../../../types"; import type { ViemClient } from "../../../utils"; import { makeMajorityVoteClient } from "./majorityVote"; export const makeCustomArbitersClient = (viemClient: ViemClient, addresses: ChainAddresses) => ({ majorityVote: makeMajorityVoteClient(viemClient, addresses), }); // Re-export static functions and types export { encodeDemand as encodeMajorityVoteDemand, decodeDemand as decodeMajorityVoteDemand } from "./majorityVote"; export type { MajorityVoteDemandData } from "./majorityVote"; ``` ### Rust SDK Extensions The Rust SDK uses a module-based architecture with macros for ABI conversions: 1. **Define the module** with DemandData types and client methods: ```rust // alkahest-rs/src/clients/arbiters/majority_vote.rs use crate::{ contracts, extensions::ContractModule, impl_abi_conversions, types::{SharedPublicProvider, SharedWalletProvider}, }; use alloy::{ primitives::{Address, Bytes, U256}, signers::local::PrivateKeySigner, }; // Implement automatic ABI encoding/decoding via .into() conversions impl_abi_conversions!(contracts::arbiters::MajorityVoteArbiter::DemandData); pub struct MajorityVoteAddresses { pub majority_vote_arbiter: Address, } #[derive(Clone)] pub struct MajorityVoteModule { signer: PrivateKeySigner, public_provider: SharedPublicProvider, wallet_provider: SharedWalletProvider, pub addresses: MajorityVoteAddresses, } impl MajorityVoteModule { pub fn new( signer: PrivateKeySigner, public_provider: SharedPublicProvider, wallet_provider: SharedWalletProvider, addresses: MajorityVoteAddresses, ) -> Self { Self { signer, public_provider, wallet_provider, addresses } } /// Cast a vote for an obligation pub async fn cast_vote( &self, obligation: alloy::primitives::FixedBytes<32>, vote: bool, demand_data: Bytes, ) -> eyre::Result { let contract = contracts::arbiters::MajorityVoteArbiter::new( self.addresses.majority_vote_arbiter, self.wallet_provider.clone(), ); let tx = contract.castVote(obligation, vote, demand_data); let pending = tx.send().await?; let receipt = pending.get_receipt().await?; Ok(receipt) } /// Get vote status for an obligation pub async fn get_vote_status( &self, obligation: alloy::primitives::FixedBytes<32>, ) -> eyre::Result<(U256, U256)> { let contract = contracts::arbiters::MajorityVoteArbiter::new( self.addresses.majority_vote_arbiter, self.public_provider.clone(), ); let status = contract.voteStatuses(obligation).call().await?; Ok((status.yesVotes, status.noVotes)) } } ``` 2. **Add to the parent module** and expose via the client: ```rust // In alkahest-rs/src/clients/arbiters/mod.rs mod majority_vote; pub use majority_vote::{MajorityVoteModule, MajorityVoteAddresses}; // In ArbitersModule, add accessor method: impl ArbitersModule { pub fn majority_vote(&self) -> MajorityVoteModule { MajorityVoteModule::new( self.signer.clone(), self.public_provider.clone(), self.wallet_provider.clone(), // addresses from config ) } } ``` ### Python SDK Extensions The Python SDK uses PyO3 to wrap Rust modules, providing Python-native interfaces: 1. **Create PyO3 bindings** that wrap the Rust module: ```rust // alkahest-py/src/clients/arbiters/majority_vote.rs use alkahest_rs::clients::arbiters::MajorityVoteModule; use pyo3::{pyclass, pymethods, PyResult}; #[pyclass] #[derive(Clone)] pub struct MajorityVote { inner: MajorityVoteModule, } impl MajorityVote { pub fn new(inner: MajorityVoteModule) -> Self { Self { inner } } } #[pymethods] impl MajorityVote { /// Cast a vote for an obligation pub fn cast_vote<'py>( &self, py: pyo3::Python<'py>, obligation: String, vote: bool, demand_data: Vec, ) -> PyResult> { let inner = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let obligation_bytes = obligation.parse() .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{}", e)))?; let receipt = inner.cast_vote(obligation_bytes, vote, demand_data.into()) .await .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{}", e)))?; Ok(format!("{:?}", receipt.transaction_hash)) }) } /// Get vote status for an obligation pub fn get_vote_status<'py>( &self, py: pyo3::Python<'py>, obligation: String, ) -> PyResult> { let inner = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let obligation_bytes = obligation.parse() .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{}", e)))?; let (yes, no) = inner.get_vote_status(obligation_bytes) .await .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{}", e)))?; Ok((yes.to_string(), no.to_string())) }) } /// Encode demand data (static method) #[staticmethod] pub fn encode_demand(voters: Vec, quorum: u64, data: Vec) -> PyResult> { use alloy::sol_types::SolValue; let voters: Vec = voters.iter() .map(|v| v.parse()) .collect::>() .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{}", e)))?; let demand = alkahest_rs::contracts::arbiters::MajorityVoteArbiter::DemandData { voters, quorum: alloy::primitives::U256::from(quorum), data: data.into(), }; Ok(demand.abi_encode()) } } ``` 2. **Expose via the ArbitersClient**: ```rust // In alkahest-py/src/clients/arbiters/mod.rs #[pymethods] impl ArbitersClient { #[getter] pub fn majority_vote(&self) -> MajorityVote { MajorityVote::new(self.inner.majority_vote()) } } ``` ### Best Practices for SDK Extensions 1. **Extract types from contract ABIs**: Use `getAbiItem` (TS) or `impl_abi_conversions!` (Rust) to derive types from contract ABIs rather than duplicating struct definitions 2. **Provide static functions**: Export `encodeDemand`/`decodeDemand` as static functions usable without instantiating a client 3. **Use hierarchical namespacing**: Organize under `client.arbiters.custom.*` or similar to match existing SDK patterns 4. **Handle async properly**: Use appropriate async patterns for each SDK (Promises in TS, async/await in Rust, PyO3 futures in Python) 5. **Add event helpers**: Include methods to watch for and parse relevant contract events 6. **Test with integration tests**: Write tests that deploy contracts and verify SDK interactions ### Example Usage Across SDKs Once implemented, developers can use your arbiter consistently: ```typescript // Encode demand (static, no client needed) import { encodeMajorityVoteDemand } from "alkahest-ts/clients/arbiters/custom"; const demand = encodeMajorityVoteDemand({ voters: [alice, bob, charlie], quorum: 2n, data: "0x" }); // Or via client instance const demand = client.arbiters.custom.majorityVote.encodeDemand({ ... }); await client.arbiters.custom.majorityVote.castVote(obligationUid, true, demand); const status = await client.arbiters.custom.majorityVote.getVoteStatus(obligationUid); ``` ```rust use alkahest_rs::contracts::arbiters::MajorityVoteArbiter; use alloy::sol_types::SolValue; // Encode demand (using .into() from impl_abi_conversions!) let demand_data = MajorityVoteArbiter::DemandData { voters: vec![alice, bob, charlie], quorum: U256::from(2), data: Bytes::default(), }; let encoded: Bytes = demand_data.abi_encode().into(); // Via client client.arbiters().majority_vote().cast_vote(obligation_uid, true, encoded).await?; let (yes, no) = client.arbiters().majority_vote().get_vote_status(obligation_uid).await?; ``` ```python # Encode demand (static method) demand = MajorityVote.encode_demand([alice, bob, charlie], 2, b"") # Or via client await client.arbiters.majority_vote.cast_vote(obligation_uid, True, demand) yes_votes, no_votes = await client.arbiters.majority_vote.get_vote_status(obligation_uid) ``` --- ## Writing Escrow Contracts Escrow contracts are used to guarantee an on-chain action upon the fulfillment of a demand. The most common use case is locking tokens to be paid out on fulfillment, but any on-chain action which you can commit to beforehand can be escrowed. In this guide, we'll walk through creating a custom escrow contract by examining the `VoteEscrowObligation` example contract located at `src/obligations/example/VoteEscrowObligation.sol`. ## Understanding the Escrow Pattern Escrow contracts in Alkahest follow a specific lifecycle: 1. **Lock Phase** (`_lockEscrow`): Verify preconditions and lock the asset/commitment 2. **Wait Phase**: Wait for someone to fulfill the demand 3. **Release Phase** (`_releaseEscrow`): Execute the escrowed action when demand is met 4. **Return Phase** (`_returnEscrow`): Handle expiration if demand is never met ## Step 1: Define Your Obligation Data Structure First, define what data your escrow needs. This struct will contain all parameters necessary for the escrow: ```solidity struct ObligationData { address votingContract; // Contract where action will occur uint256 proposalId; // Specific target for the action uint8 support; // Action parameters (0=Against, 1=For, 2=Abstain) address arbiter; // Who verifies fulfillment conditions bytes demand; // Arbiter-specific verification data } ``` The `arbiter` and `demand` fields are required - they determine what conditions must be met to release the escrow. ## Step 2: Inherit from BaseEscrowObligation Your contract must inherit from `BaseEscrowObligation` and register an attestation schema: ```solidity import {BaseEscrowObligation} from "../../BaseEscrowObligation.sol"; contract VoteEscrowObligation is BaseEscrowObligation { constructor( IEAS _eas, ISchemaRegistry _schemaRegistry ) BaseEscrowObligation( _eas, _schemaRegistry, // Define your attestation schema "address votingContract,uint256 proposalId,uint8 support,address arbiter,bytes demand", true // revocable ) {} } ``` ## Step 3: Implement Data Encoding/Decoding Create helper functions to encode and decode your obligation data: ```solidity function encodeObligationData( ObligationData memory data ) public pure returns (bytes memory) { return abi.encode( data.votingContract, data.proposalId, data.support, data.arbiter, data.demand ); } function decodeObligationData( bytes memory data ) public pure returns (ObligationData memory) { // Decode the data back into your struct ( address votingContract, uint256 proposalId, uint8 support, address arbiter, bytes memory demand ) = abi.decode(data, (address, uint256, uint8, address, bytes)); return ObligationData({ votingContract: votingContract, proposalId: proposalId, support: support, arbiter: arbiter, demand: demand }); } ``` ## Step 4: Implement extractArbiterAndDemand This required method tells the base contract how to find the arbiter information: ```solidity function extractArbiterAndDemand( bytes memory data ) public pure override returns (address arbiter, bytes memory demand) { ObligationData memory obligation = decodeObligationData(data); return (obligation.arbiter, obligation.demand); } ``` ## Step 5: Implement \_lockEscrow This method is called when creating an escrow. Verify preconditions and track the escrow: ```solidity function _lockEscrow(bytes memory data, address from) internal override { ObligationData memory obligation = decodeObligationData(data); // Example: Check user has delegated voting power IVotingContract votingContract = IVotingContract(obligation.votingContract); require( votingContract.delegates(from) == address(this), "Must delegate before creating escrow" ); // Track the escrow using a deterministic hash bytes32 dataHash = keccak256(data); escrowedVoter[dataHash] = from; escrowedData[dataHash] = obligation; emit VoteDelegated(dataHash, from, obligation.votingContract); } ``` **Important**: The `_lockEscrow` method runs with `msg.sender` as the escrow contract itself. If you need the user to be `msg.sender` (like for token transfers), handle those in a separate transaction before creating the escrow. ## Step 6: Implement \_releaseEscrow This executes when the demand is fulfilled. Perform the escrowed action: ```solidity function _releaseEscrow( bytes memory escrowData, address to, // The fulfiller bytes32 fulfillmentUid ) internal override returns (bytes memory result) { ObligationData memory obligation = decodeObligationData(escrowData); bytes32 dataHash = keccak256(escrowData); // Get the original escrower address voter = escrowedVoter[dataHash]; require(voter != address(0), "No active escrow"); // Execute the escrowed action (e.g., cast a vote) IVotingContract votingContract = IVotingContract(obligation.votingContract); votingContract.castVoteWithReason( obligation.proposalId, obligation.support, string(abi.encodePacked("Vote cast on behalf of ", voter)) ); // Clean up delete escrowedVoter[dataHash]; // Return any relevant data return abi.encode(obligation.proposalId, obligation.support, to, voter); } ``` ## Step 7: Implement \_returnEscrow Handle expiration and cleanup when escrows are reclaimed: ```solidity function _returnEscrow(bytes memory data, address to) internal override { bytes32 dataHash = keccak256(data); if (escrowedVoter[dataHash] != address(0)) { // Clean up storage delete escrowedVoter[dataHash]; delete escrowedData[dataHash]; emit VoteReturned(dataHash, to); // Note: Cannot automatically return delegated voting power // because only the user can call delegate() (msg.sender check) } } ``` ## Step 8: Add Public Entry Points Create user-friendly functions for creating escrows: ```solidity function doObligation( bytes calldata data, uint64 expirationTime ) external returns (bytes32 uid) { // Call the base implementation uid = doObligationRaw(data, expirationTime, bytes32(0)); // Track any additional mappings you need bytes32 dataHash = keccak256(data); attestationToDataHash[uid] = dataHash; return uid; } // Overload for typed data function doObligation( ObligationData memory obligationData, uint64 expirationTime ) external returns (bytes32 uid) { bytes memory encodedData = encodeObligationData(obligationData); return this.doObligation(encodedData, expirationTime); } ``` ## TypeScript SDK Extension Create a TypeScript client for your escrow contract: ```typescript import { getAttestedEventFromTxHash, type ViemClient } from "../utils"; import type { ChainAddresses, Demand } from "../types"; interface VoteData { votingContract: `0x${string}`; proposalId: bigint; support: number; } export const makeVoteEscrowClient = ( viemClient: ViemClient, addresses: ChainAddresses & { voteEscrowObligation: `0x${string}` }, ) => ({ createVoteEscrow: async ( vote: VoteData, demand: Demand, expiration: bigint, ) => { const hash = await viemClient.writeContract({ address: addresses.voteEscrowObligation, abi: voteEscrowAbi.abi, functionName: "doObligation", args: [ { votingContract: vote.votingContract, proposalId: vote.proposalId, support: vote.support, arbiter: demand.arbiter, demand: demand.demand, }, expiration, ], }); const attested = await getAttestedEventFromTxHash(viemClient, hash); return { hash, attested }; }, collectVoteEscrow: async ( escrowAttestation: `0x${string}`, fulfillment: `0x${string}`, ) => { const hash = await viemClient.writeContract({ address: addresses.voteEscrowObligation, abi: voteEscrowAbi.abi, functionName: "collectEscrowRaw", args: [escrowAttestation, fulfillment], }); return hash; }, }); ``` ## Rust SDK Extension Create a Rust client for your escrow contract: ```rust use alloy::primitives::{Address, FixedBytes}; use alloy::rpc::types::TransactionReceipt; pub struct VoteData { pub voting_contract: Address, pub proposal_id: u64, pub support: u8, } impl VoteEscrowModule { pub async fn create_vote_escrow( &self, vote: &VoteData, demand: &ArbiterData, expiration: u64, ) -> eyre::Result { let vote_escrow_contract = contracts::VoteEscrowObligation::new( self.addresses.vote_escrow_obligation, &self.wallet_provider, ); let receipt = vote_escrow_contract .doObligation( contracts::VoteEscrowObligation::ObligationData { voting_contract: vote.voting_contract, proposal_id: vote.proposal_id.into(), support: vote.support, arbiter: demand.arbiter, demand: demand.demand.clone(), }, expiration, ) .send() .await? .get_receipt() .await?; Ok(receipt) } } ``` ## Python SDK Extension Create a Python client for your escrow contract: ```python from eth_abi import encode from web3 import Web3 class VoteEscrowClient: def __init__(self, web3: Web3, addresses: dict): self.w3 = web3 self.vote_escrow_address = addresses['vote_escrow_obligation'] async def create_vote_escrow( self, vote_data: dict, demand: dict, expiration: int = 0 ): """ Create a vote escrow. Args: vote_data: Dict with voting_contract, proposal_id, support demand: Dict with arbiter and demand bytes expiration: Unix timestamp for expiration (0 = no expiration) """ contract = self.w3.eth.contract( address=self.vote_escrow_address, abi=VOTE_ESCROW_ABI ) tx = await contract.functions.doObligation( ( vote_data['voting_contract'], vote_data['proposal_id'], vote_data['support'], demand['arbiter'], demand['demand'] ), expiration ).build_transaction() return await self.w3.eth.send_transaction(tx) ``` ## Usage Example Here's how users interact with your escrow contract: ```typescript // Step 1: User prepares by delegating (if required) await votingContract.delegate(voteEscrowAddress); // Step 2: Create escrow const escrow = await client.voteEscrow.createVoteEscrow( { votingContract: daoAddress, proposalId: 42n, support: 1, // Vote FOR }, { arbiter: trivialArbiterAddress, // Arbiter that checks conditions demand: "0x", // Arbiter-specific data }, 0n, // No expiration ); // Step 3: Someone fulfills the demand const fulfillment = await createFulfillmentAttestation(); // Step 4: Collect escrow to execute the action await client.voteEscrow.collectVoteEscrow(escrow.attested.uid, fulfillment.uid); // Alternative: Reclaim if expired await client.voteEscrow.reclaimExpired(escrow.attested.uid); ``` ## Key Considerations ### Precondition Verification Always verify that users have completed necessary setup before creating escrows. For example, the VoteEscrowObligation requires users to delegate voting power first. ### Message Sender Context Remember that `_lockEscrow`, `_releaseEscrow`, and `_returnEscrow` execute with the escrow contract as `msg.sender`. If you need user-initiated actions (like token approvals), handle those separately. ### Cleanup After Expiration The `_returnEscrow` method cannot always undo all actions automatically. Users may need to take manual steps after reclaiming expired escrows. The `VoteEscrowObligation` example at `src/obligations/example/VoteEscrowObligation.sol` is a complete implementation of the example in this tutorial. --- ## Writing Fulfillment Contracts Fulfillment contracts are specialized obligation contracts that create attestations representing completed work, delivered results, or fulfilled conditions. These contracts work in tandem with arbiters to enable conditional release of escrowed assets or execution of on-chain actions. ## Using StringObligation The simplest pattern for fulfillment uses generic contracts like `StringObligation` that can store arbitrary string data. This approach prioritizes flexibility and rapid prototyping over structure and type safety. **Pattern illustrated**: Using generic data containers for fulfillments where the validation logic is entirely handled by external arbiters (often off-chain oracles). ### Example: API-like Compute Jobs with Generic Results This example demonstrates submitting unstructured results that are validated by trusted oracles: ```solidity // Step 1: Create escrow with query for oracle validation bytes memory innerDemand = abi.encode("GET weather NYC"); bytes memory demand = abi.encode( TrustedOracleArbiter.DemandData({ oracle: oracleAddress, data: innerDemand }) ); bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: paymentToken, amount: paymentAmount, arbiter: address(trustedOracleArbiter), demand: demand }), expirationTime ); // Step 2: Submit result as simple string StringObligation.ObligationData memory resultData = StringObligation .ObligationData({ item: "72F, sunny" // Unstructured result }); bytes32 fulfillmentUid = stringObligation.doObligation( resultData, escrowUid // Reference to the escrow being fulfilled ); // Step 3: Oracle validates the string result trustedOracleArbiter.arbitrate(fulfillmentUid, true); // Step 4: Claim escrow with validated fulfillment erc20EscrowObligation.collectEscrow(escrowUid, fulfillmentUid); ``` **When to use this pattern**: - Rapid prototyping where data format is still evolving - Simple text-based results (answers, summaries, translations) - Cases where all validation logic lives off-chain - Maximum flexibility is more important than on-chain queryability **Trade-offs**: - ✅ Maximum flexibility - any string data can be stored - ✅ Simple to implement and modify - ✅ Low gas costs for simple data - ❌ No on-chain structure or validation - ❌ Difficult to query or index specific fields - ❌ All parsing must happen off-chain ## Fulfillment contracts that don't implement IArbiter More sophisticated fulfillment patterns use specialized contracts that enforce structure without implementing validation logic. This separation of concerns creates cleaner, more maintainable systems. **Pattern illustrated**: Creating domain-specific fulfillment contracts that provide structure and helper functions while delegating validation to separate arbiter contracts. ### Example: Specialized API Result Contract This example shows a fulfillment contract specialized for API-like results with rich metadata: ```solidity contract ApiResultObligation is BaseObligation { struct ObligationData { string endpoint; // API endpoint called string method; // HTTP method uint16 statusCode; // Response status string headers; // Response headers string body; // Response body uint256 timestamp; // When the call was made } // Helper functions for structured queries function isSuccessfulCall(bytes32 uid) external view returns (bool) { ObligationData memory data = getObligationData(uid); return data.statusCode >= 200 && data.statusCode < 300; } function matchesEndpoint( bytes32 uid, string calldata endpoint ) external view returns (bool) { ObligationData memory data = getObligationData(uid); return keccak256(bytes(data.endpoint)) == keccak256(bytes(endpoint)); } // Convenience method for common cases function doSimpleObligation( string calldata endpoint, string calldata body, bytes32 refUID ) external returns (bytes32) { ObligationData memory data = ObligationData({ endpoint: endpoint, method: "GET", statusCode: 200, headers: "Content-Type: application/json", body: body, timestamp: block.timestamp }); return doObligation(data, refUID); } } ``` Usage example: ```solidity // Submit structured result ApiResultObligation.ObligationData memory resultData = ApiResultObligation.ObligationData({ endpoint: "https://api.weather.com/v1/NYC", method: "GET", statusCode: 200, headers: "Content-Type: application/json", body: '{"temperature": "72", "unit": "F"}', timestamp: block.timestamp }); bytes32 fulfillmentUid = apiResultObligation.doObligation( resultData, escrowUid ); // Oracle can now validate with rich context bool isSuccess = apiResultObligation.isSuccessfulCall(fulfillmentUid); bool correctEndpoint = apiResultObligation.matchesEndpoint( fulfillmentUid, expectedEndpoint ); ``` **When to use this pattern**: - Production systems requiring consistent data formats - Need for on-chain queryability and indexing - Multiple validation criteria based on different fields - Complex domains with rich metadata requirements **Benefits of separation from IArbiter**: 1. **Single Responsibility**: Fulfillment contracts focus on data storage and access 2. **Reusability**: Same fulfillment format can work with different arbiters 3. **Upgradeability**: Can deploy new arbiters without changing fulfillment format 4. **Composability**: Can combine multiple fulfillment types with logical arbiters ### Design Considerations When choosing between generic and specialized fulfillment contracts: **Use Generic (StringObligation) when**: - Data format is undefined or rapidly changing - Validation is entirely off-chain - Simplicity is paramount - Gas optimization for simple data **Use Specialized contracts when**: - Data has consistent structure - On-chain validation or queries are needed - Multiple parties need to interpret the data - Building production-grade systems ### Advanced Pattern: Typed Fulfillment Registry For systems with many fulfillment types, consider a registry pattern: ```solidity contract FulfillmentRegistry { mapping(string => address) public fulfillmentContracts; function registerFulfillmentType( string calldata typeName, address contractAddress ) external onlyOwner { fulfillmentContracts[typeName] = contractAddress; } function getFulfillmentContract( string calldata typeName ) external view returns (address) { address contract_ = fulfillmentContracts[typeName]; require(contract_ != address(0), "Unknown fulfillment type"); return contract_; } } ``` This enables dynamic fulfillment types while maintaining structure: - Requesters specify desired fulfillment type in demands - Providers use the appropriate contract for their fulfillment - Oracles know how to parse each registered type - New types can be added without modifying existing contracts ## Fulfillment contracts that implement IArbiter Some fulfillment contracts have an obvious, inherent way to validate their data that makes sense to bundle directly with the obligation format. This pattern combines the obligation and arbiter functionality when the validation logic is tightly coupled to the data structure itself. **Pattern illustrated**: Bundled validation for cases where there's a single, canonical way to verify the fulfillment data (e.g., cryptographic signatures, mathematical proofs, or other deterministic validation). ### Example: Cryptographic Signature Verification This example demonstrates a combined Obligation/IArbiter contract that handles cryptographic signature verification: ```solidity contract CryptoSignatureObligation is BaseObligation, IArbiter { struct DemandData { address publicKey; // Required signer bytes32 challenge; // Message to sign string domain; // Optional EIP-712 domain } struct ObligationData { bytes signature; // The cryptographic proof address signerAddress; // Who created the signature uint256 timestamp; // When signed string metadata; // Additional context } // Implements IArbiter to self-validate function checkObligation( Attestation memory obligation, bytes memory demand, bytes32 fulfilling ) external view override returns (bool) { DemandData memory demandData = abi.decode(demand, (DemandData)); ObligationData memory obligationData = decodeObligationData( obligation.data ); // Verify signer matches requirement if (obligationData.signerAddress != demandData.publicKey) { return false; } // Verify signature cryptographically return _verifySignature( demandData.challenge, obligationData.signature, demandData.publicKey, demandData.domain ); } function _verifySignature( bytes32 challenge, bytes memory signature, address expectedSigner, string memory domain ) internal pure returns (bool) { // Extract signature components bytes32 r; bytes32 s; uint8 v; assembly { r := mload(add(signature, 32)) s := mload(add(signature, 64)) v := byte(0, mload(add(signature, 96))) } // Create message hash bytes32 messageHash = keccak256( abi.encodePacked("\x19Ethereum Signed Message:\n32", challenge) ); // Recover and verify signer address recoveredSigner = ecrecover(messageHash, v, r, s); return recoveredSigner == expectedSigner; } } ``` Usage example: ```solidity // Step 1: Create escrow requiring Bob's signature bytes memory demand = cryptoSigObligation.encodeDemand( bob, // Bob must sign keccak256("Authenticate"), // Challenge to sign "" // No domain ); bytes32 escrowUid = erc20EscrowObligation.doObligation( ERC20EscrowObligation.ObligationData({ token: paymentToken, amount: paymentAmount, arbiter: address(cryptoSigObligation), // Self-validating demand: demand }), expirationTime ); // Step 2: Bob creates signature off-chain bytes32 messageHash = keccak256( abi.encodePacked("\x19Ethereum Signed Message:\n32", challenge) ); (uint8 v, bytes32 r, bytes32 s) = signMessage(messageHash); // Off-chain bytes memory signature = abi.encodePacked(r, s, v); // Step 3: Submit signature as fulfillment bytes32 fulfillmentUid = cryptoSigObligation.doObligation( CryptoSignatureObligation.ObligationData({ signature: signature, signerAddress: bob, timestamp: block.timestamp, metadata: "Authentication proof" }), escrowUid ); // Step 4: Claim escrow (self-validates via checkObligation) erc20EscrowObligation.collectEscrow(escrowUid, fulfillmentUid); ``` **When to use this pattern**: - The fulfillment format has one obvious way to validate it - Validation logic is inherently tied to the data structure - The same validation will always be used (no alternative interpretations) - Examples: cryptographic signatures, hash preimages, mathematical proofs **When NOT to use this pattern**: - Generic data formats that could be validated many different ways (like `StringObligation`) - When validation logic might need to be updated or swapped - When different use cases require different validation criteria - When validation depends on external state or oracle data **Trade-offs**: 1. **Simplicity**: Single contract for both storage and validation 2. **Tight Coupling**: Can't swap validation logic without new contract 3. **Clear Intent**: The validation method is part of the fulfillment definition 4. **Gas Consideration**: Validation logic affects every claim transaction ### Design Principles for Combined Obligation/Arbiter Contracts When deciding whether to implement IArbiter in a fulfillment contract: **Good Candidates**: - Cryptographic operations (signatures, hashes, proofs) - Mathematical validations (ranges, formulas, checksums) - Self-contained proofs with deterministic verification - Domain-specific formats with canonical validation **Poor Candidates**: - Generic containers (strings, bytes, JSON-like structures) - Business logic that may evolve - Validations requiring external data or consensus - Formats used across different validation contexts **Implementation Guidelines**: - Keep `doObligation()` and `checkObligation()` logic clearly separated - Validation should be deterministic and state-independent - Consider gas costs of validation in every claim - Document why validation is bundled rather than separate ### Advanced Pattern: Proof Aggregation For complex scenarios requiring multiple proofs: ```solidity contract MultiProofObligation is BaseObligation, IArbiter { struct ProofSet { bytes[] proofs; uint256 proofType; // Bitmap of proof types included } function checkObligation( Attestation memory obligation, bytes memory demand, bytes32 counteroffer ) external pure override returns (bool) { ProofSet memory proofs = abi.decode(obligation.data, (ProofSet)); RequiredProofs memory required = abi.decode(demand, (RequiredProofs)); // Verify each required proof type is present and valid for (uint i = 0; i < required.types.length; i++) { if (!_verifyProof(proofs, required.types[i])) { return false; } } return true; } } ``` This pattern enables: - Multiple proof requirements in single fulfillment - Flexible proof combinations - Efficient batch validation - Extensible proof types ### Choosing Between Patterns **Separate Arbiters (like with StringObligation)**: - Maximum flexibility - any arbiter can validate - Clean separation of concerns - Validation logic can evolve independently - Same format works with different validation strategies **Combined Obligation/IArbiter (like CryptoSignatureObligation)**: - Natural fit when validation is inherent to the format - Simpler deployment (one contract instead of two) - Clear semantics - the validation is part of the definition - Reduced flexibility - validation is fixed The choice isn't about sophistication but about coupling: use combined contracts when the validation method is fundamental to what the fulfillment represents, and separate contracts when the data format and validation logic naturally vary independently. --- ## Alkahest Alkahest is an open-source public good for on-chain peer-to-peer agreements, built on [EAS](https://attest.org/). Everything is dissolving. Goods and services become increasingly discrete and non-fungible as their provision is decentralized. One seller at a particular time does not demand nor offer exactly the same as another selling a similar thing, and negotiation must become case-specific and fine-grained. At the same time, the atomization of provision means that aspects of negotiation, scheduling, settlement, and quality-control formerly unilaterally and opaquely controlled by sell-side monopolies can now be shared within an open ecosystem. Alkahest is a toolkit for dissolving consolidated economies and creating atomic ones, providing a set of lightweight and composable units for common processes in peer-to-peer negotiation and exchange. Solve et coagula.
Agreement flow Escrow Flow Walk through common agreement patterns from token trades to composed demands. Token Trading Job Trading Composing Demands
Decision logic Writing Arbiters Implement on-chain arbiters and off-chain oracle services for custom release criteria. On-chain Arbiters Off-chain Oracles
Contracts Escrow and Fulfillment Build escrow contracts and fulfillment contracts around the Alkahest lifecycle. Writing Escrow Contracts Writing Fulfillment Contracts
--- ## Documentation Start with product documentation for the shipped protocol surface, or read the Modular Markets docs for the pattern language behind compositional markets.
Product docs Alkahest Escrow flows, arbiter patterns, and fulfillment contracts. Escrow Flow Writing Arbiters Contracts Open Alkahest docs ->
Theory and patterns Modular Markets Compositional Game Theory fundamentals, patterns, and applications. Fundamentals Patterns Applications Open Modular Markets docs ->
--- ## Introducing Arkhai > **Historical vision — November 2025** > > This post preserves Arkhai's publication-time thesis and exploration plan. Its bespoke-contract, onboarding, product, and roadmap language is not a statement of current SCM behavior, live supply, customer adoption, or an active funnel. For current compute scope, see [Arkhai Compute](https://compute.arkhai.io/), the [SCM source](https://github.com/arkhai-io/simple-compute-market), or a [general inquiry](https://www.arkhai.io/contact). Customs formed from millennia of human commerce are being overturned. Rules rewritten and intuitions discarded, we are hurdling into a revolution in economics, charting a new era of digital markets driven by cybernetic agents. We will have trouble understanding these agents. They will create synthetic assets alien to our experiences, negotiate at superhuman speeds, and cooperate and compete to form an emergent superintelligence. Orchestration of compute, storage, bandwidth, information, energy, and capital will be driven to the limits of physics, leading to an unprecedented pace of scientific breakthroughs. Arkhai revolutionizes what lies at the core of this transformation: markets. The markets of yesteryear will not suffice in this new epoch of economics. Our modular marketplace primitives solve this problem by enabling the construction of a wide array of marketplaces from basic building blocks. Our name, Arkhai, comes from the Greek for 'first principles': the fundamental elements that ancient Greek philosophers sought as the basis of all reality. We chose this name because we're providing the elemental components that compose into complex market structures, not prescribing the structures themselves. We enable agents to negotiate bespoke contracts with each other on the fly. They don't just execute pre-defined templates - they can actively negotiate terms, set custom conditions, and create unique agreements for each transaction. While economists and game theorists cast shadows of "utility-maximizing" agents for their audiences, they themselves did not even understand the figures they held in their hand. Since the advent of game theory in the early 20th century, we have since developed algorithms which can indeed approximate ideal behaviour in strategic environments (with all the necessary caveats). We are building the agents long dreamt of; where before we only saw shadows, we now see glimpses of light. ## Shipping Today - [Alkahest smart contracts](https://github.com/arkhai-io/alkahest), which enable: - Escrow with Boolean-algebraic release and recursive arbitration - Attestation‑driven obligations to prove delivery/work - Primitives for agent-to-agent credible commitments - SDKs in [TypeScript](https://github.com/arkhai-io/alkahest-ts), [Rust](https://github.com/arkhai-io/alkahest-rs), and [Python](https://github.com/arkhai-io/alkahest-py), plus [docs](https://www.arkhai.io/docs) to get your escrow up and running fast ## The Plan - The November 2025 plan proposed launching smart contracts for sophisticated agentic commerce. - It proposed exploring participation by compute and energy providers. - It proposed training agents to optimize resources over compute-energy topologies. - It proposed exploring participation by scientists who needed compute or wanted to contribute to open-source reproducible RAG pipelines of the scientific literature. - It proposed launching markets for scientific computing futures. ## Join Us With Arkhai's infrastructure, the bottleneck shifts from capability to imagination. The cybernetic economy is already here. The only choice left is whether to help define the terms. Developers: Read the current [Arkhai documentation](https://www.arkhai.io/docs). Compute buyers, sellers, builders, and market operators: [Explore Arkhai Compute](https://compute.arkhai.io/) or [inspect SCM source](https://github.com/arkhai-io/simple-compute-market). For any current evaluation path: [submit a general inquiry](https://www.arkhai.io/contact). Website: [arkhai.io](https://arkhai.io) \ GitHub: [github.com/arkhai-io](https://github.com/arkhai-io) \ X: [@arkhai_io](https://x.com/arkhai_io) \ LinkedIn: [linkedin.com/company/arkhai-io](https://www.linkedin.com/company/arkhai-io) \ Contact: [arkhai.io/contact](https://www.arkhai.io/contact) --- ## The Agent-First Future: Why Machines Need Their Own Economic Infrastructure Eighteen Months and a Million Dollars: Part 1 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Current marketplace infrastructure is built for humans, not for the agent-driven future > - Prior distributed computing marketplaces failed for structural reasons: duplicated efforts, unsustainable economics, case-specific designs that don't generalize > - Agent-native design is crucial: protocols built so machines can observe, act, receive rewards, and improve over time > - Arkhai's architecture is built on three primitives: bundle exchange, credible commitments, and agent-to-agent negotiation > - The same primitives that enable compute markets also enable energy, storage, bandwidth, and many other asset classes > - Where this leads: composable markets and collective intelligence emerging from agent interactions ## The future is autonomous The February 2026 thesis opened with a hypothetical: at 2 AM, an AI research assistant needs more GPU capacity to finish a protein folding simulation before a morning meeting. It could search across multiple sources, negotiate with multiple providers, commit funds to escrow, and start the job while its owner sleeps. That scenario described a direction, not a capability available through SCM at publication or today. Some component technologies exist; connecting them into a dependable market remains engineering and deployment work. Most economic activity in the future will be undertaken by machines, yet most of our marketplaces are built for humans. APIs exist for many digital marketplaces (stock exchanges, energy markets, centralized cryptocurrency exchanges) but in practice, these serve institutions and wealthy individuals. They're not built for autonomous agents operating at machine speed, making thousands of decisions per second, on behalf of millions of users. Neither centralized nor decentralized marketplaces can currently support the types of economic activity that will be possible with autonomous agents in the coming years. Arkhai is building something different. The timing is critical. ## Why now A number of developments converge to make agent-native infrastructure possible now: - After decades of development and multiple AI winters, agents can finally act. As new model architectures replace large chunks of human labor, the agents running these workloads will need to acquire resources and coordinate with other agents. - Assets are becoming programmable. The tokenization of real-world assets is bringing traditional markets (energy, commodities, metals, retail, etc.) onto blockchain infrastructure. This creates the foundation for machine-readable, machine-tradeable economic activity. - Decades of mechanism design research (the study and design of incentive structures) are becoming increasingly implementable. Previously confined to academic journals, these ideas can now be instantiated in protocols. The tools exist to design markets that make sense for machine participants. The current industrial-scale compute and energy buildout is one of the largest infrastructure projects in history, but resource allocation remains human-native. Data centers are proliferating, but not fully utilized: a wasted resource continues to waste away - idle computing power. And not just in data centers - also on desktops, laptops, phones, and IoT devices. But this trash is merely waiting to be turned into treasure, and all that's needed are the right markets, and the right agents. ## Why prior approaches failed Many distributed computing marketplaces have failed since the writing of the whitepaper upon which this post is based. Below is a brief, partial exploration of the challenges that cryptocurrency-based marketplaces faced. First, the economics were unsustainable. The underlying costs of hardware and electricity (and land in some cases) are denominated in fiat. To maintain token prices, fiat buy pressure is needed. But utilization rates were low, failing to balance token emissions. Thus, economic models that attempted to onboard computing power through supply-side economics struggled. Second, token lock-in made things worse. Such a strong restriction made it so that developers, companies, and others had to believe in the success of inchoate protocols in order to commit to using the marketplaces in the long-term. Having the marketplace as a moat via token lock-in hampered adoption rather than reinforced it. Third, many protocols created similar marketplace infrastructure from scratch. This duplication wasted resources. Most of these marketplaces have since died, unable to achieve the network effects needed for sustainability. Fourth, while the marketplace infrastructure was needlessly duplicated, the case-specific collateralization these protocols implemented didn't generalize and were very difficult/expensive to modify. Fifth, these marketplaces were built for humans, not machines. Sign-in flows, KYC requirements, and manual approvals designed for human browsing patterns. All create friction that blocks machine participation, despite the fact that agents will likely be the largest consumers of compute in the coming years. Even where APIs exist, they're interfaces for humans using software, not for software operating autonomously. Sixth, pricing and scheduling were treated as separate concerns. A client might pay more for faster results, or less to receive them later. A compute node faces many job offers with varying requirements, prices, and deadlines. These problems are intertwined, but most or all distributed computing protocols treat them as independent. Seventh, most or all of these marketplaces treated compute as an isolated asset, without even accounting for the other two pillars of modern computing - storage and bandwidth. Eighth, verification doesn't scale. How do clients know they're getting correct results? Cryptographic methods are slow and expensive. Secure enclaves have exploits. Optimistic verification suffers from collusion. Each approach has tradeoffs, and very few if any protocols implemented architectures that allowed market participants to choose which verification strategy they wanted, if any at all. ## Marketplaces for everything Compute is the starting point, not the destination. The same primitives that enable a compute marketplace also enable markets for energy, storage, bandwidth, information, and real-world assets. Storage and bandwidth follow immediately from compute as the other two pillars of modern computing: retrieval markets that enable paying for data serving; bandwidth guarantees for latency-sensitive applications; agents coordinating across geographies need to negotiate network capacity alongside compute and storage in order to optimize resources. Integrating storage and bandwidth into compute marketplaces increases the utility of the latter. Energy is another natural extension. AI inference is power-hungry. As agents proliferate, energy markets become essential. Peer-to-peer energy trading, utility-scale trading, trading between data centers: all require the same or similar commitment and negotiation primitives as compute markets. Information markets let agents pay for access to proprietary datasets, real-time feeds, or specialized models. And of course, real-world assets - the components out of which these machines are ultimately made: rare-earth metals and semiconductors, marketplace listings, futures contracts for commodities. These are necessary for agents to truly maximize revenue on behalf of their owners. The goal is generic, composable marketplace infrastructure that works for any asset type. Start with compute, generalize the primitives, and the same architecture enables storage, bandwidth, energy, information, and beyond. ## Game theory foundations To build markets that work for machines, we need to start from first principles. The concept of "agents" can in some ways be traced to the foundations of game theory, and the assumption of rational actors capable of perfectly executing actions that maximize their benefit. Below are some basic concepts of game theory that inspired Arkhai's protocol design. Agents have utility functions that output how much value they'd get from receiving certain objects. They take whatever action is necessary to maximize their return. This is utility maximization, and it's the starting assumption. From here, the question becomes: how do you design a reward structure that incentivizes agents to reveal their honest preferences? This is incentive compatibility. Without it, agents mask their true preferences, and market mechanisms can lead to worse outcomes than in the counterfactual case. The gold standard is strategyproofness: incentive structures in which no agent benefits from dishonesty regardless of what other agents do, and every agent has an incentive to participate. This is the target for market design. The revelation principle tells us this target is reachable. Under certain assumptions, any game with an equilibrium can be transformed into a game with the same equilibrium, but where the incentive mechanism is strategyproof. The field that studies all of this is mechanism design: the study and design of incentive structures. If game theory studies which actions lead to maximum reward, mechanism design studies which reward structures give rise to desired actions. These basic concepts are what are required to build agentic economies that actually work, for it is in the very context of these game-theoretic concepts that agents became defined. ## What agents need Mechanism design gives us the tools to understand how to design markets. But how do agents actually evolve in these markets? They need to learn. For that, reinforcement learning is the best tool currently in use. In reinforcement learning, agents operate with four primitives: the environment (evolving state), actions (available choices), transitions (probability of state changes given actions), and rewards (what agents receive for outcomes). Building these primitives into protocol design means agents can observe, act, get rewards, and improve. In the case of compute marketplaces, a compute node maximizing return faces a classic computer science problem known as the scheduling problem. In contrast to static APIs with fixed prices, agent-driven marketplaces require nodes to consent to running jobs. That means that price must be incorporated from both the client's and compute node's perspectives. Thus, the pricing and scheduling of compute jobs are intertwined, and we reframe both in terms of utility maximization - a situation well-suited for reinforcement learning. Finally, agents also need to form coalitions. Nodes are limited by their hardware configurations and may want to accomplish goals beyond their individual capabilities. Combining resources to achieve larger goals requires coordination. To enable all of this, Arkhai's architecture was originally designed with three primitives in mind: - The first primitive is the exchange of arbitrary bundles of assets. Not single assets, but bundles for bundles (of course, single assets are possible, and are just a special case of the more general infrastructure). This enables complex multi-asset deals that single-asset exchanges can't express. - The second primitive is agreements modeled by a series of credible commitments. Many protocols require collateralization with different types of collateral deposited, withdrawn, or slashed depending on outcomes. These rules can be abstracted to a series of credible commitments, where participating parties deposit collateral that moves based on events. This replaces case-specific escrow with a more general pattern. - The third primitive is agent-to-agent negotiation. Nodes propose matches to each other directly. They can accept, reject, or counter-propose until a conclusion is reached. We started with these three primitives and found that the first two are better unified into a single abstraction. We'll explore that evolution in a later post. Composability of markets means that the same primitives used for compute markets can be layered into more complex economic structures: energy markets that interact with compute markets, knowledge markets that interact with both. ## Where this leads Generic marketplaces let agents trade the very physical components they're made of. Blockchains and cryptocurrencies are trending towards multi-agent systems operating in multi-token economies. Tokenization of real-world assets drives this, along with the separation of concerns that multiple tokens enable. Out of these primitives naturally emerge multi-agent systems that represent the desires of both humans and machines, forming the foundation of a new, decentralized digital economy. These primitives also unlock markets where none existed before. Idle computing power sits wasted everywhere: in data centers, on desktops, laptops, phones, IoT devices. The right marketplace infrastructure can turn this latent capacity into tradeable assets. This raises a question: what computations might someone pay for later, even if no one will pay for them now? Futures markets for idle compute, retroactive funding for speculative work. These become possible when marketplace primitives are general enough to express them. Agents will negotiate with each other on behalf of their human owners. For many real-world assets, this negotiation will be exchanges of fixed data schemas. But for intents-based negotiations, language model-based agents will likely create their own languages when communicating with each other. And just as new languages will emerge from agent interactions, synthetic assets will emerge as agents negotiate over human-designed assets and encounter their limitations. Where this ultimately leads is a decentralized collective machine intelligence which can guide itself through market forces towards a self-improving cybernetic system. Out of these multi-agent systems will emerge collective intelligence: agents that coordinate resources across continents, optimize for objectives humans specify but couldn't achieve alone, and discover solutions no individual agent could find. Many of the components for this exist today, all that's needed are the right coordination mechanisms. ## What this means for you If you're building infrastructure that needs marketplace capabilities, you face a choice: build primitives from scratch, or use components designed for composability. What you get: - Explore how composable marketplace infrastructure could be deployed without reinventing every primitive - Your market, your rules - Agent-native design for machine-speed operations - Generalizable primitives that work across asset types If you're building applications that need compute (AI workloads, scientific computing, or any computationally intensive task), distributed compute markets could, when deployed with real supply, broaden access to computational resources. If you're researching multi-agent systems, mechanism design, or decentralized coordination, this direction could enable experiments in economic environments. Agents that learn to trade, form coalitions, and optimize objectives remain a research path. ## The series ahead This is the first in a series exploring the ideas in the Arkhai whitepaper. By February 2026, development had translated parts of that vision into software and experiments; it was not evidence of production systems or a deployed market. In the posts ahead, we'll cover why existing compute marketplaces fail and what we're doing differently. We'll introduce Alkahest, our programmable escrow system that emerged from unifying our original three primitives into two. We'll dig into verification (how do you trust results from machines you don't control?), collateral markets (how do you price jobs when costs are unknown?), and adversarial design (we train agents to cheat so we can stop them). We'll explore tokenizing idle compute and retroactive funding models. And we'll show you what you can build today. The future is autonomous. The infrastructure for that future needs to be machine-native. The agents are coming. The question is whether our economic infrastructure will be ready for them. --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Why Current Distributed Compute Marketplaces Are Broken Eighteen Months and a Million Dollars: Part 2 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Many distributed computing marketplaces fail for structural reasons, not just execution issues > - Underutilization + fiat-denominated costs create unsustainable tokenomics; supply-side economics without demand doesn't hold > - Web3 compute differs from Web2: node consent reshapes pricing and scheduling > - Duplicated marketplaces waste resources and likely consolidate into a few prominent solutions > - Marketplace-as-moat strategies (fees, token lock-in) hamper adoption; competitive dynamics push toward no-fee and token-agnostic designs > - Compute markets without built-in storage and bandwidth capabilities will fall behind those that integrate the full stack ## The graveyard is full of good ideas The aggregation of compute power is becoming its own industry. But the path to decentralized compute markets is littered with failures. In our last post, we listed a set of structural problems with distributed computing marketplaces. Many projects have run into those problems and died. The graveyard is full of protocols with solid technical foundations, talented teams, and genuine vision. They failed anyway. This is not about execution. The obstacles are not surface-level bugs or marketing missteps. They are architectural. Below is a brief, partial exploration of the dominant failure modes described in the whitepaper. ## The broken flywheel Traditional supply-side economics in Web3 compute tends to follow a predictable loop: First, a protocol subsidizes supply through token emissions. The pitch is simple: "Bring your GPUs, earn tokens." Second, node count grows quickly. The network looks large on paper. Third, utilization rates remain low. Capacity exists, but demand does not materialize at the pace needed to justify the supply. Fourth, the underlying costs of hardware, networking, and electricity remain denominated in fiat. With low utilization, token prices often depend on external buy pressure the system itself does not generate. Fifth, node operators start exiting. Token price declines. More operators leave. The flywheel runs in reverse. This cycle has repeated across a number of protocols. The names differ. The shape doesn't. ## Why demand never came Economic models that attempt to onboard large amounts of computing power through supply-side economics are increasingly becoming outdated; their insufficiencies are evidenced by the lack of demand to counterbalance the supplies they provide. In practice, the obstacles that most developers and users face in adopting DCNs and DePINs often do not even touch verifiable computing. The integration and operational friction is high enough that verifiability does not even enter the conversation. Some of that friction is self-inflicted. Many marketplaces were built for humans: dashboard-first workflows, sign-in flows, and manual approval steps, and in some cases KYC. Even where APIs exist, they often assume a human operator. That blocks the machine-native path where agents discover, negotiate, and execute jobs autonomously. Demand for verifiability will likely increase as popularity, and thus incentive to exploit DCNs, grows (this is also true of highly in-demand traditional compute providers), but it is unclear at what pace this trend will progress. And when verification does matter, it doesn't scale cleanly. Proof systems can be slow or costly, TEEs have well-known attack surfaces, and optimistic schemes face collusion and dispute overhead. Many protocols pick one approach and force it on everyone, instead of letting participants choose the risk, latency, and cost tradeoff per job. Outside the whitepaper, a useful contrast is the non-Web3 "neocloud" pattern: pick a narrow initial customer segment and build to meet its needs end-to-end before scaling supply. Many Web3 protocols tried to bootstrap generalized supply first and expected demand to catch up. ## The duplication problem Most Web3 distributed computing networks and DePINs are creating relatively complex marketplaces from scratch. This duplication results in a massive waste of resources, especially since much of the infrastructure is open source and easily forked. Due to competitive dynamics and without substantial changes, the industry will likely consolidate into a small number of prominent solutions. Many protocols build their own: - Matching engine - Pricing algorithms - Collateral schemes - Dispute resolution - Payment channels - Reputation systems - Hardware provisioning - Networking stack This is wasteful. Open-source marketplace infrastructure should be shared, not reinvented. Differentiation should be in what you are trading and how you verify it, not in rebuilding the entire trading infrastructure from first principles. Even worse, collateral rules tend to be bespoke and rigid. They are expensive to modify, hard to extend to new deal types, and hard to reason about as requirements change. Open source should make reuse easy. In practice, it often doesn't, because the marketplace was seen as the moat. ## Marketplace as moat doesn't work Since developers, companies, and others must believe in the long-term success of these protocols before committing, and are unlikely to have large stakes in these protocols, having the marketplace as a moat (via token lock-in or fee-based structures) is more likely to hamper adoption than reinforce it. Competitive dynamics may push marketplaces toward no-fee and token-agnostic designs over time, especially as the ecosystem trends toward chain-agnosticism and account abstraction. For these reasons, Arkhai aspires to build the primitives for exchange as a public good: no fees, no token lock-in, token-agnosticism, and multi-chain compatibility. ## The consent problem In Web2, the client controls the nodes on which the compute is being run. In Web3, compute nodes have to consent to having computations run on them. This changes the nature of scheduling, because now it is necessary to incorporate the price of a job from the perspectives of both client and compute node. While bid-based scheduling has been used in distributed computing protocols before, introducing actual money into the system changes how the scheduling problems need to be approached. First, pricing and scheduling are intertwined. Deadlines, prices, verification requirements, and collateral constraints all shape whether a node will accept a job and whether a client should submit it. Second, the mechanisms must treat nodes as utility-maximizers, not obedient workers. If consent is treated as a minor implementation detail, you can end up with thin liquidity and inconsistent service: nodes will decline jobs that don't meet their constraints, and reliability becomes an emergent side effect rather than a design property. ## The fragmented stack problem Compute marketplaces that don't have built-in capabilities for handling storage and bandwidth will fall behind those that do. A compute job needs more than compute. Where does the input data come from? Where do results go? How does data move between client, node, and storage? How do jobs that span multiple nodes communicate? Protocols that only provide compute expect developers to integrate separate storage protocols, bandwidth solutions, and coordination layers, each with its own tokens, APIs, and trust models. The integrated stack matters not because it is impossible to assemble pieces from different protocols, but because the integration complexity becomes a barrier to adoption. Every additional protocol is another potential failure point, another interface to learn, and another place incentives can break. ## What this means for you If you're building infrastructure that needs compute capabilities, the failure modes of previous protocols offer a clear path forward: - Start with demand. Pick a narrow initial customer segment and meet its needs end-to-end before scaling supply. - Avoid rebuilding basic marketplace infrastructure. Use modular components that already exist. Differentiate on verification, node network quality, matching, and reliability. - Avoid token lock-in as a default. Let people try your product without economic commitment. Make it easy to integrate, easy to test, and easy to adopt. - Design for consent. Your nodes are independent actors with their own incentives. Pricing and scheduling are the same problem viewed from different angles. - Integrate the stack. Compute alone is not enough. Storage and bandwidth need to be part of the solution, not afterthoughts. The compute marketplace failures of the past five years were not bad luck. They were predictable outcomes of structural decisions. Different decisions lead to different outcomes. ## Next in the series We've looked at what failed and why. In our next post, we'll explore what worked: the architectural decisions that led from three primitives to two, and the introduction of Alkahest, the programmable escrow system that emerged from unifying bundle exchange and credible commitments into a single, more powerful abstraction. --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Two Primitives for Machine-Native Markets Eighteen Months and a Million Dollars: Part 3 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - In implementation, the whitepaper's three primitives effectively became two: Alkahest (escrow + arbitration) and agent-to-agent negotiation > - Bundle exchange collapsed into credible commitments: lock value, validate conditions, then release (or escalate) > - Alkahest expresses deals as Statements, validators, and escrowed value > - Off-chain solvers, on-chain auctions, and agent-to-agent negotiation differ mainly in where matching happens and what it costs > - We bias toward agent-to-agent negotiation because deals have terms, not just price, and we want to avoid match-making chokepoints > - Open source, no fees, token-agnostic: exchange primitives as a public good ## From three primitives to two Architecture is destiny. The primitives you choose determine what you can build. In this post, we'll do three things: (1) explain what happened when we tried to implement the whitepaper's primitive set, (2) define Alkahest at the interface level we use throughout the series, and (3) outline three market-making patterns, with an emphasis on why we bias toward agent-to-agent negotiation. In our first post, we described three primitives at the core of Arkhai's architecture: the exchange of arbitrary bundles of assets, agreements modeled by a series of credible commitments, and agent-to-agent negotiation. In our second, we looked at why many existing compute marketplaces came short of expectations. Now let's look at the February design direction and what had been learned along the way. On paper, "bundle exchange" and "credible commitments" look distinct. One moves assets. The other enforces trust: collateral deposited, conditions evaluated, outcomes enforced. In code, bundle exchange kept collapsing into the credible-commitments machinery. An exchange is just escrow plus conditions: parties lock value, a validator checks whatever needs to be checked, and funds move. When "whatever needs to be checked" isn't fully on-chain, that validator becomes an arbiter and the deal becomes an arbitration problem. Once we had an escrow-and-arbitration system that could express a *series* of credible commitments, the bundle-exchange primitive wasn't really a primitive anymore. It was one configuration of the same system. That left two primitives that mattered in practice: (1) a generalized escrow/arbitration layer for exchange-with-commitment (Alkahest), and (2) a way for agents to find and agree on deals (agent-to-agent negotiation). ## The case against specialization This realization didn't come from theory. It came from staring at the collateralization patterns across different marketplace types. Across the protocols we studied, the structure kept reducing to the same simple state machine: 1. Parties deposit value. 2. Conditions are evaluated. 3. Value is released, refunded, slashed, or escalated based on outcomes. The specifics vary: in compute marketplaces, the condition might be job completion plus verification; in energy markets, delivery confirmation; in a simple trade, mutual consent. But the structure stays the same. The degrees of freedom are: what is deposited, what is checked, and where value flows. Many protocols require collateralization with different types of collateral deposited, withdrawn, or slashed depending on the outcomes of some events. These sets of rules can be abstracted to a series of credible commitments, where participating parties each deposit collateral, and that collateral moves based on events. Once we saw this, the evolution from three primitives to two became hard to avoid. Bundle exchange is a special case of credible commitments, and credible commitments collapse into one programmable escrow-and-arbitration system. ## Introducing Alkahest This unified abstraction is Alkahest: programmable escrow and arbitration with arbitrary conditions. In the repository, we describe it plainly: a contract library and SDKs for validated peer-to-peer escrow. That's the interface we care about. The name comes from alchemy. The alkahest was the theorized universal solvent, a substance that could dissolve anything. It's an appropriate metaphor: Alkahest dissolves bespoke exchange logic into a general-purpose primitive, and lets you recompose it into many market types. Solve et coagula. Concretely, there are three moving parts: - **Statement**: an obligation. One party commits value, specifying who benefits, what amount is held, and under what conditions it should be released. Statements are the fundamental unit of commitment in the system. - **Validator**: the mechanism that determines when conditions are satisfied. A validator might check for manual confirmation from a designated party, data from an oracle feed, the passage of time, or any composable combination. When conditions can't be resolved automatically, validators can escalate to arbitration. - **Escrow**: the component that holds value until validators confirm conditions are met, then automatically releases funds to the appropriate party. If conditions aren't met, escrow can refund, slash, or follow whatever path the parties programmed at the start. This is credible commitments made concrete. And bundle exchange falls out as a special case: parties lock assets, validators check conditions, and escrow releases value atomically. ## What two primitives enable The following consequences were February 2026 generalized design proposals, not a list of current SCM capabilities or deployed integrations. Alkahest for exchange-with-commitment (and arbitration), and agent-to-agent negotiation for matchmaking. Together, these two primitives enable a surprising range of applications. A few immediate consequences follow: - The three main categories of verifiable computing can all plug into Alkahest's validator layer: check whether the computation was done correctly, release payment if yes, and slash collateral if no. - Storage and bandwidth can be incorporated into the same marketplace structure as compute. They are additional assets with their own validators and conditions, not separate "side protocols" developers have to stitch together. - Clearinghouse-style settlement becomes expressible. A party can act as a central counterparty, net positions across many trades, and settle through the same escrow and arbitration layer. - Peer-to-peer bartering is just another configuration. Agents can swap arbitrary bundles with conditional release, without a trusted intermediary. - Natural language agreements become viable. When parts of a deal can't be fully machine-verified, you can still make them machine-actionable by precommitting to arbitration (human, committee, or even AI-based), rather than pretending every condition is deterministic. - Agents interact with a consistent interface across market types, which makes it easier to reuse agent scaffolding (state, actions, rewards) when moving between compute, energy, storage, bandwidth, or barter. - Most marketplace types reduce to programming different conditions into the same contracts. This is the power of generalization. Rather than building a different escrow for compute, another for energy, another for peer-to-peer trading, you build one escrow that can express all of them. ## Three ways to make a market This section compares publication-time market-making directions. Current SCM uses signed, bilateral, policy-governed scalar-price negotiation in which non-price VM terms remain fixed or validated; it does not implement arbitrary cross-asset negotiation. With Alkahest handling the exchange layer, the second primitive handles how parties find each other. The whitepaper outlines three broad methods of market-making: off-chain solvers, on-chain auctions, and agent-to-agent negotiation. Each comes with tradeoffs in trust models, efficiency, and scalability. The main difference is where matching happens. First, off-chain solvers. Nodes send bids and asks to a solver layer that proposes matches, which are then settled on-chain. This can be fast, and implemented as a competitive set of solvers rather than a single privileged matcher. The tradeoff is that you introduce an off-chain coordination layer that can become a chokepoint for liveness, censorship resistance, and integration complexity. You also introduce a self-interested party that needs a business model. In practice, that often means extracting value from matching (fees, spreads, preferential routing), which can skew incentives and concentrate power around whoever controls order flow. Second, on-chain auctions. Bids and asks go on-chain, and the matching rule executes under consensus. This provides strong transparency and auditability. The tradeoff is that matching inherits blockchain latency and gas costs, and strategies become more legible to everyone watching the chain. Third, agent-to-agent negotiation. Nodes propose matches to each other directly. They can accept, reject, or counter-propose until a conclusion is reached. This avoids a central matching engine, but it requires more sophisticated agents and can take more messages to converge. The upside is that negotiation is a flexible coordination substrate. With the same underlying infrastructure, you can represent RFQs, posted-price deals, barter, and even auction-like dynamics by changing agent policies, without rewriting settlement. And because settlement treats resources as assets with conditions, the same negotiation machinery can carry across asset types like compute, storage, bandwidth, or energy, including bundles that mix them. The February design direction favored negotiation as the default because: - DCN deals are heterogeneous; terms matter as much as price (deadlines, verification, collateral, escalation paths). - We want to avoid concentrating order flow in a single matching service. - Negotiation composes cleanly with Alkahest: once two agents agree, the escrow/arbitration layer enforces the outcome. The architecture still supports solver- and auction-based matching where it makes sense. Alkahest doesn't care how the match was made. It only cares that both parties agreed and that conditions are met. ## Open infrastructure This brings us to a design philosophy that runs beneath the architecture. Arkhai aspires to build the exchange primitives as a public good: no fees, no token lock-in, token-agnosticism, and multi-chain compatibility. This can sound counterintuitive. If the exchange layer is free and open source, where is the competitive advantage? The advantage is the technology, not the base mechanics. Better verification. Smarter matching. More reliable nodes. The applications and services built on top of the open infrastructure. Open source means other teams can build on Alkahest instead of rebuilding escrow and dispute resolution from scratch. The ecosystem grows faster. Fewer bugs, because the code is audited by more eyes. The total number of marketplaces and use cases expands, which benefits everyone building on the infrastructure. Generalization over specialization. Open primitives over locked gardens. This is how you build infrastructure that lasts. ## What this means for you If you're building with these primitives, a few implications are immediate: - If you're building a marketplace, Alkahest replaces your escrow layer. Define your conditions, choose your validators, deploy. You don't need to build collateralization logic from scratch, and you don't need to rediscover the edge cases that other protocols have already found and fixed. - If you're building a compute protocol, Alkahest's programmable conditions can express a wide range of verification schemes: cryptographic proofs, TEE attestations, replication-based checking, or hybrid approaches. Payment becomes conditional on verification using the same contracts. - If you're designing incentive structures, the composable structure lets you treat a "mechanism" as a program: what information counts (validators), what happens when conditions are met or disputed (escrow + arbitration), and how offers are made (negotiation policies). That makes it easier to do ablations and simulation: change one rule, rerun agent-based experiments, and measure how incentives shift, without rebuilding the whole system. It also makes comparisons cleaner, because different mechanisms can share the same settlement primitive. ## Next in the series Alkahest handles exchange and commitment. Agent-to-agent negotiation handles matchmaking. But one question keeps coming up: how do you trust computation done by a machine you don't control? In our next post, we'll dig into verification. Three categories of verifiable computing, the local-to-global consensus spectrum, and the honest answer that every approach involves tradeoffs. --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Trust Without Trustees Eighteen Months and a Million Dollars: Part 4 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - A central question in decentralized compute: how do you trust computation done by a machine you don't control? > - Three categories of verifiable computing: cryptographic methods, secure enclaves (TEEs), and replication > - Local vs global consensus is a spectrum, not a binary choice > - In replication-based verification, local consensus can, in principle, drive the number of replications needed down to 1 > - Approximate agreement can handle non-deterministic computations > - Every verification method is some combination of slow, inefficient, expensive, or insecure: the honest answer is tradeoffs ## Verifying compute you didn't run Outsourcing compute is easy. Outsourcing trust is not. In our last post, we introduced Alkahest and the two-primitive architecture. Alkahest handles exchange and commitment. Agent-to-agent negotiation handles matchmaking. But there's a question we've been circling since the first post in this series: how do you trust computation done by a machine you don't control? In a trustless, permissionless distributed computing marketplace, verifiability becomes central. How do clients know that they are getting the correct results returned to them, if not through the massive replication of Byzantine Fault Tolerance traditionally offered by blockchains? This is the subject of a subfield of computer science known as verifiable computing, and it's arguably one of the hardest problems in decentralized infrastructure. The constraint is clear: verifying the result should have less overhead than computing the result in the first place. If verification costs more than doing the work yourself, there's no point in outsourcing it if you could handle it alone. Different applications need different trust levels. A financial transaction might require cryptographic certainty. A scientific simulation might accept statistical confidence. A rendering job might only need visual inspection. And many customers don't want verification at all; they want a counterparty they trust. This is one key reason non-Web3 neoclouds have been able to win: they start with a narrow set of customer needs and rely on reputation, contracts, and recourse instead of "trustless" verification. One-size-fits-all verification doesn't exist, and pretending otherwise is how protocols end up building something nobody can actually use. In what follows, we'll break verification into three broad categories, then frame consensus as a spectrum (not a binary), then look at what happens when computations aren't deterministic, and finally be explicit about the tradeoffs. ## Three categories There are three main categories of verifiable computing: cryptographic methods, secure enclaves (TEEs), and replication-based methods. First, cryptographic methods. These rely on mathematics for their security. Zero-knowledge proofs let a prover convince a verifier that a computation was done correctly without revealing private inputs. Multi-party computation allows joint computation without exposing individual inputs. Fully homomorphic encryption enables computation over encrypted data. These methods provide the strongest guarantees, but they are also among the slowest and most expensive. For many practical workloads today, cryptographic verification costs far more than the computation itself. Second, secure enclaves (TEEs). These are isolated environments where code and data are insulated from the rest of the system, including the operating system. The goal is to maintain both confidentiality and integrity. TEEs have a promising future, especially as exploits continue to be patched, but even the known exploits continue to limit applicability. Third, replication-based methods (often called "optimistic" verification). These rely on recomputing the work and checking whether results match. This is the simplest approach to understand and implement, but it comes with extra computational cost. With proper incentive design, the overhead of recomputing can be reduced while still enabling network scaling. Replication often requires game-theoretic mechanisms like collateral slashing and reputation layers to counter cheating. ## The consensus spectrum The verification question is tied to a deeper question about consensus: how many entities need to agree that a computation was done correctly? At one end of the spectrum is global consensus. Blockchains traditionally achieve consensus through massive replication across all participating nodes. This provides strong guarantees: Byzantine Fault Tolerance ensures the network reaches agreement even when some nodes act maliciously. But this level of replication becomes too costly when the corresponding level of security is not needed. At the other end is local consensus, which at a minimum only requires a single agent to be convinced of the state. For a two-sided marketplace, only the client really needs to be convinced that a computation was done correctly. But these computations are not done in isolation. The interrelation between clients choosing nodes, and the need for new clients to arrive and be assured that cheating is disincentivized, implies the creation of a global game that, while not requiring global consensus in the traditional sense, emulates it. Between these extremes are hybrid designs. Local versus global consensus is not a binary choice but a spectrum. The middle involves agreement among a subset of nodes, perhaps within a specific region or cluster, which can lead to faster and more efficient decision-making through reduced communication overhead. Systems can balance speed, efficiency, and security based on their specific requirements. One of the primary benefits of local consensus, in the context of replication-based verification, is that market incentives can, in principle, be structured to drive the total number of replications needed for verifiability down to one. This minimizes computational cost, financial cost, and energy consumed. Getting to a single replication is the target: it means every unit of compute goes toward useful work, not redundant checking. ## When computations aren't deterministic Note: There has been significant research and practical progress on this topic since the whitepaper was completed in summer 2024. This section primarily summarizes the whitepaper's framing. Replication-based verification sounds straightforward: run the job twice, compare results. But this assumes determinism. If the same program on two different machines produces exactly the same output, the comparison is trivial. What happens when the computations are not deterministic? Neural network inference on different hardware can produce different results. Floating point arithmetic varies across architectures. Parallel workloads with race conditions produce different orderings. Once outputs differ, the real question becomes: what does "match" mean? One option is approximate agreement: if the verification yields a result close enough to the original, the result is considered valid. BOINC (the Berkeley Open Infrastructure for Network Computing) has been using approximate agreement for a long time. The approach works, but it requires an application-specific distance measure, which introduces developer overhead. Approximate agreement might apply to some neural networks. In large language models, it might be possible to measure the distance between outputs before decoding. But slightly different outputs before decoding can still lead to large differences after decoding, which leaves room for abuse. Another option is objective-function-based evaluation. Proof-of-Work is a classic example: solving a cryptographic puzzle is hard, but verifying the solution is cheap. The proof is in the result itself. Similar approaches work for problems where the answer is easy to check even if it's hard to find. And sometimes the right answer is to trust the compute node and not verify at all. This doesn't count as verifiable computing, by definition; however, reputation systems have decades of research behind them, and for many applications the trust tradeoff is acceptable. ## Honest about limitations Every lock can be picked. Every verification approach has weaknesses. - Cryptographic methods for verifiable computing are very slow and will almost certainly always be much more expensive than bare metal execution for many workloads. - Secure enclaves have exploits, and while the future is promising as patches continue, they are not ready for highly sensitive applications. - Optimistic verification suffers from issues of determinism, and even when those are solved or approximated away, there remain deep issues of collusion. Verifiable computing options are, at present, some combination of slow, inefficient, expensive, or insecure. This isn't a problem to be solved by picking the "right" method. Every method trades one thing for another. The most flexible approach is to support all three categories and let applications choose the verification that matches their risk tolerance, performance requirements, and budget. Arkhai's design enables the incorporation of all of these methods. Alkahest's programmable conditions don't care whether the validator is checking a zero-knowledge proof, a TEE attestation, or the output of a replication check. The verification layer is modular. Swap in different validators for different trust levels. The shortcomings of all of these approaches will decline with time and increasing use. Cryptographic methods are getting faster. TEE exploits are getting patched. Replication mechanisms are getting smarter about collusion. But waiting for a perfect solution means shipping nothing. The pragmatic choice is to build the system that supports all approaches and improves as each approach matures. ## What this means for you If you're building on decentralized compute, the verification question will define your architecture. Don't pick a verification method in the abstract. Start with your application's requirements. How sensitive is the data? How expensive is the computation? What's the cost of a wrong result? One coarse way to think about it: - For low-stakes workloads like rendering, batch processing, or data transformation, replication with reputation may be sufficient. - For moderate-stakes workloads like scientific simulation or model training, approximate agreement or TEE-backed execution can provide stronger guarantees. - For high-stakes workloads like financial computation or medical inference, cryptographic methods may be worth the overhead. These aren't mutually exclusive. A single marketplace can support all three categories, with applications selecting the trust level they need. This is what Alkahest's modular validator system enables. ## Next in the series We've covered how Alkahest handles exchange and commitment, and how different verification methods handle trust. But there's a practical problem we haven't addressed: collateral. When a compute node takes a job, how much collateral should it deposit? What if the computational cost isn't known ahead of time? In our next post, we'll introduce collateral markets and show how programmable escrow handles the unknown. --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Collateral Markets and Programmable Escrow Eighteen Months and a Million Dollars: Part 5 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Collateralization enables trustless protocols by leveraging credible commitments > - The core problem for batch jobs: how much collateral do you put up when the computational cost isn't known ahead of time? > - The collateral multiplier: commit to a multiplier M, deposit (price x M) after job completion > - A series of credible commitments (timeout, payment, cheating) replaces case-specific escrow with one general pattern > - Collateral markets let agents negotiate over collateral itself, not just jobs > - The combination of credible commitments and collateral markets forms the foundation of generic, machine-actionable marketplaces ## The problem with "just put up collateral" Collateral is the teeth behind trust. Verification tells you what happened, and collateral makes it matter. In our last post, we looked at verification: how do you trust computation done by a machine you don't control? We covered three categories of verifiable computing, the consensus spectrum, and the honest answer that every approach involves tradeoffs. But verification doesn't exist in a vacuum. Verification needs enforcement. If a compute node returns a wrong result, something must happen. In most protocols, that something is collateral slashing: the node put up a deposit, and the deposit gets taken away if the node cheats. Collateralization facilitates trustless protocols by leveraging the credible commitments enabled by blockchains. It's used whenever protocol actors want an incentive for counterparties to behave honestly. In blockchains secured by Proof-of-Stake, nodes securing the network deposit collateral to ensure the blocks they propose are legitimate. In compute marketplaces, a client that wants assurance some task will be done correctly may require the compute node to deposit collateral. Likewise, the compute node may require the client to deposit collateral ensuring it will be paid if it does the work correctly. In this post, we'll do three things: (1) show why fixed upfront collateral is an unsatisfactory solution when costs are unknown a priori, (2) introduce the collateral multiplier as a market parameter, and (3) show how multiple risk types become a series of credible commitments expressed in the same escrow abstraction. ## When the cost is unknown Imagine you're a compute node. A potential client wants to run a machine learning training job. How much collateral should you put up? The job might converge in ten minutes. It might run for six hours. You won't know until it's done. Note: This framing is about batch-style jobs that settle on completion. Time-metered payments are often more straightforward, because the protocol can settle periodically instead of estimating total cost upfront. One major problem in many verification-via-replication protocols is the issue of how much collateral to put up for a job where the computational cost is not known ahead of time. This is more common than you'd think. Running a machine learning training job might take minutes or hours depending on convergence. A scientific simulation might complete quickly or hit edge cases that extend runtime. Even a straightforward data processing pipeline can encounter unexpected data volumes. If you require fixed upfront collateral, you face a dilemma: - Set it too low, and nodes have insufficient incentive to complete the job honestly: the reward from cheating exceeds the cost of losing their deposit. - Set it too high, and you exclude smaller nodes from participating, concentrating the market among well-capitalized operators. Neither option leads to a healthy marketplace. Fixed upfront collateral doesn't work when computational costs are variable. ## The collateral multiplier This problem can be solved with collateral markets. Rather than depositing a fixed amount of collateral ahead of time (or topping up opportunistically), a compute node commits to a collateral multiplier at the time of deal agreement. After a job is completed, the compute node deposits into escrow the amount it will charge the client times the collateral multiplier. In other words: collateral becomes a ratio, not a guess. If the job turns out to be cheap, the absolute collateral is low. If it's expensive, the collateral scales proportionally. The collateral multiplier solves the issue of knowing ahead of time how much collateral is required. It enables at least primitive forms of optimistic verification for programs where the computational cost or runtime is not known in advance. And it creates a market for collateral over which agents can negotiate. That last point is significant. The multiplier isn't fixed by the protocol. It's a parameter that buyer and seller negotiate. A client who needs strong assurance might demand a high multiplier. A node confident in its reliability might offer a low one. The multiplier becomes a signal of trust, and the market determines the price of that trust. ## A series of credible commitments The collateral multiplier handles one problem: unknown costs. But real marketplace interactions involve multiple types of risk, each requiring its own form of collateral. Consider a verification-via-replication protocol. There are at least three types of collateral at play: - **Timeout collateral** ensures the node completes the job within an agreed timeframe. It's deposited when the deal begins and refunded if the job finishes on time. If the node fails to deliver, the timeout collateral is slashed. - **Payment collateral** ensures the client will pay for completed work. The client deposits funds that transfer to the node upon successful verification. If the client disputes a correct result, arbitration can resolve the claim. - **Cheating collateral** ensures the node doesn't return fabricated results. If verification reveals that the node cheated, this collateral is slashed and partially distributed to the verifier as a bounty. Each of these collaterals is deposited at particular times, and is refunded or slashed at other times, based on events that happen on-chain. Each deal is not "one escrow", but a small program: multiple deposits, multiple conditions, and multiple possible outcomes. These sets of rules can be abstracted to a series of credible commitments, where participating parties each deposit collateral, and that collateral moves based on events. This is where Alkahest comes in. Rather than building custom collateral logic for each of these types, Alkahest's Statement and Validator system expresses all of them. Each collateral type is a Statement with its own conditions and validators. The timeout validator checks deadlines. The payment validator checks job completion. The cheating validator checks verification results. Same contracts, different conditions. Same infrastructure, different markets. ## Why this matters for marketplace design Case-specific collateralization architectures are an outdated approach. Every protocol that builds its own collateral system is rebuilding the same patterns: deposit, condition, release or slash. The differences are in the specifics, not the structure. The combination of a series of credible commitments and collateral markets dramatically enhances what's possible. You can build a compute marketplace, an energy marketplace, and a data marketplace on the same collateral infrastructure. Each uses different validators, but the underlying pattern is the same. This is the power of Alkahest applied to a concrete problem. In Post 3, we described Alkahest as a unified abstraction for exchange and commitment. Here, you can see what that abstraction gets you in practice: a single escrow system that handles timeout penalties, payment guarantees, cheating prevention, and any other collateral type a marketplace might need, all through programmable conditions. ## What this means for you If you're building a marketplace that requires any form of collateral, you don't need to design your own escrow state machine. Alkahest's escrow and arbitration can express your collateral types. Define the deposit conditions, the verification events, and the outcome rules. The infrastructure handles the rest. If you're building a compute protocol specifically, the collateral multiplier pattern solves the variable-cost problem. Nodes commit to a multiplier. Jobs settle based on actual cost. The market sets the price of trust through the multiplier negotiation. If you're thinking about market design more broadly, collateral markets represent a recursive pattern. Agents negotiate over jobs. Jobs require collateral. Collateral levels are negotiated. This creates a market within a market, which is exactly the kind of composability that generic primitives enable. ## Next in the series We've built up the mechanism layer: Alkahest for exchange and commitment, verification for trust, collateral markets for economic guarantees. But how do you test these mechanisms against adversaries? In our next post, we'll look at Arkhai's approach to adversarial design: training agents to cheat so we can stop them. Game-theoretic white-hat hacking, multi-agent inverse reinforcement learning, and the honest acknowledgment that convergence isn't guaranteed. --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Designing for Adversaries Eighteen Months and a Million Dollars: Part 6 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Analytic approaches to verification-via-replication make assumptions that break down in practice > - Much of the academic literature (and many public protocols it inspired) simplifies away real-world complexity: hardware heterogeneity, network topologies, latencies, failures, repeated games, collusion > - Arkhai's research approach: train agents to cheat, then iterate mechanisms to stop them > - Multi-agent RL trains attackers and defenders; reward-design ideas (including inverse RL) help search for incentives that elicit desired behavior > - Honest about limitations: multi-agent training is not guaranteed to converge, and empirical robustness is not a proof > - Three possible outcomes: defense works, arms race, or learning that a given mechanism class is insufficient under the threat model. All three are valuable. > **Note** > The approach described in this post originated as research and should not be read as current SCM implementation, deployed infrastructure, or production evidence. ## Why analytic approaches fail Mathematical proofs are reassuring, but the real world is unforgiving. In our last post, we covered a collateral-market proposal: how a collateral multiplier might address unknown costs, and how a series of credible commitments might handle different types of risk. This was a conceptual mechanism direction, not evidence of a deployed layer. But mechanisms need to survive contact with adversaries. How do you know a collateral scheme actually prevents cheating? How do you know a verification protocol can't be gamed by colluding nodes? The traditional approach is to mathematically prove that honest behavior is rational and leads to Nash equilibrium. The problem is that analytic results depend on tractable models, and tractable models depend on simplifications. In our reading, much of the academic literature on verification-via-replication (and the public protocols it has inspired) does not fully account for the operational complexity of real distributed computing networks, including: - different hardware configurations - network topologies - variable latencies - node failures - repeated games - collusion Many papers and protocols make assumptions about the environment, action spaces, or failure models that, once relaxed, weaken or eliminate the original theoretical guarantees. This isn't a criticism of the research. It's the nature of the problem. Real distributed systems violate simplifying assumptions constantly. For this reason, Arkhai explores an alternative approach that forgoes analytic guarantees in favor of empirical evaluation. ## Game-theoretic white-hat hacking If you can't prove your mechanism is secure, you can test it. Arkhai's research approach to verification-via-replication is to train agents to maximize their utility, including cheating and/or colluding if necessary. Once these nodes are strong enough to find real weaknesses, mechanisms can be iterated and evaluated against the strategies the agents actually discover. In practice, this starts in simulation. You build a digital twin of the protocol environment and let agents explore the strategy space safely before you ever trust the results in a live network. Operationally, this becomes a loop: 1. Train the best attackers you can. 2. Observe the strategies they discover. 3. Update the mechanism (incentives, penalties, verification rules). 4. Retrain and repeat. This inverts the typical workflow. Instead of designing a mechanism and hoping it's robust, you start by training the attackers. You let them find the weaknesses. You watch how they exploit the system. Then you design countermeasures and test whether the attackers can adapt. The approach treats protocol design as adversarial competition. Red team versus blue team. The red team is trained to find exploits. The blue team is trained to close them. The protocol improves through iteration. This requires the same agent-based primitives we described in our first post: environments (the protocol state), actions (cheating strategies, honest behavior, coordination with other nodes), transitions (how the protocol responds), and rewards (the utility function the attacker is trying to maximize). States, actions, transitions, and rewards are treated as first-class citizens in the architecture. ## Multi-agent inverse reinforcement learning The attacker/defender training loop draws heavily on multi-agent reinforcement learning, where each agent's learning changes the environment for the others. Standard reinforcement learning asks: given a reward structure, what actions maximize reward? Inverse reinforcement learning asks the opposite: given observed behavior (typically demonstrations), what reward structure could have produced it? Multi-agent inverse reinforcement learning extends this to groups of agents. It finds reward structures that elicit particular actions from collections of agents: for example, not cheating and not colluding in verification-via-replication-based distributed computing networks. This is mechanism design through machine learning. Rather than deriving incentive structures analytically, you explore them empirically through adversarial testing. Trained attackers show you where the vulnerabilities are. The reward-design loop suggests what incentive changes might close those vulnerabilities. These tools are not a silver bullet, but they are a promising way to explore incentive landscapes in settings where analytic modeling breaks down. And the space of incentive structures goes well beyond verifiable computing. Coalition formation, resource markets, negotiation protocols: anywhere agents have overlapping interests (sometimes cooperative, sometimes competitive, sometimes both), multi-agent RL and reward-learning can be useful. ## Convergence From a theoretical perspective, multi-agent learning dynamics in such environments are not guaranteed to converge to stable equilibria. There are convergence results in restricted settings, but there is no general guarantee that adversarial training will yield mechanisms robust to manipulation. Multi-agent training can be unstable. Agents adapt to each other. The best strategy for attacker A depends on what defender B does, which depends on what A does, which depends on B. The feedback loops can cycle without settling. Convergence proofs that work for single-agent RL do not extend straightforwardly to multi-agent settings, in part because each agent faces a moving target. This doesn't mean the approach is useless. We're not claiming to have solved adversarial mechanism design. We're claiming that this method provides empirical evidence about mechanism robustness, with clear acknowledgment that the evidence is not a guarantee. Three outcomes are possible. First, the adversarial training finds attacks, the mechanism updates successfully defend against them, and further adversarial training fails to find new attacks. This is the best case: you have empirical confidence that the mechanism is robust against the class of attacks your training can discover. Second, the adversarial training and defensive updates enter an arms race. Each side keeps adapting. This is less satisfying, but still useful: you've learned that the mechanism requires ongoing maintenance, and you have a process for that maintenance. Third, the adversarial training finds attacks that we can't defend against within the mechanism class we're exploring. That doesn't imply that *no* mechanism can exist; it suggests that our current assumptions, primitives, or threat model are insufficient. This outcome is still valuable: it tells you to change the design space (different verification, different trust assumptions, different market structure), rather than endlessly tuning parameters. All three outcomes produce useful knowledge. That's the advantage of empirical methods: they don't just tell you whether you succeeded, they tell you what kind of problem you're dealing with. ## What this means for you If you're building protocols that need adversarial resistance, consider what guarantees you can actually provide. Formal proofs are reassuring, but they rest on assumptions that may not hold in production. Empirical testing doesn't give you proofs, but it gives you evidence from realistic conditions. If you're integrating with Arkhai's infrastructure, expect adversarial evaluation to be part of how mechanisms are validated over time. Formal analysis matters, but the goal is to complement it with empirical evidence from simulated adversaries. If you're researching multi-agent systems, multi-agent RL and reward-learning tools are underutilized in protocol design. Most mechanism design still happens analytically. There's room for empirical methods to become standard practice. ## Next in the series We've covered the full mechanism stack: Alkahest for exchange and commitment, verification for trust, collateral markets for economic guarantees, and adversarial design for robustness testing. In our next post, we'll step back from mechanisms and look at economics. Where does idle compute fit into this picture? What happens when you tokenize latent capacity and create futures markets for work that has no buyer yet? --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## Tokenizing Idle Compute Eighteen Months and a Million Dollars: Part 7 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Idle computing power sits wasted everywhere: data centers, desktops, laptops, phones, IoT devices > - SETI@home, Folding@home, and BOINC showed volunteers would contribute compute for free > - Volunteer compute can create real value, but contributors typically receive points or recognition, not economic upside > - Application-specific token markets can finance compute in real time: contributors can sell into stablecoins > - Immutable records enable retroactive rewards: reward contributors after success materializes > - The IP tension: some outputs need to remain private. Secure enclaves enable verifiable credit with confidentiality. ## Trash waiting to become treasure The world is full of idle compute. Data centers run underutilized. Desktops sit idle overnight. Phones and edge devices spend most of their time waiting. Most of that capacity is already provisioned and paid for. When it sits idle, it's economic trash: cycles that could be doing useful work. The right markets turn that trash into treasure. Volunteer computing proved that some of this capacity can be mobilized. It also revealed the ceiling: once you've saturated the set of people willing to donate for free, the supply stops growing, and it still represents only a small fraction of the idle compute that exists. We've spent the past several posts on mechanism design: Alkahest for exchange and commitment, verification for trust, collateral markets for economic guarantees, adversarial training for robustness. Now we can ask the economic question those mechanisms are in service of: How do you create markets for work that has no buyer yet, but might be valuable later? In what follows, we'll use BOINC as a precedent, name the value capture problem, and then outline two complementary mechanisms: (1) application-specific token markets for real-time liquidity, and (2) retroactive rewards backed by immutable contribution records and privacy-preserving attribution. ## What BOINC taught us The history of grid-based distributed scientific computing goes back at least to the mid-1990s, including distributed prime-search projects like GIMPS, and later to more mainstream platforms like SETI@home and Folding@home. Both of the larger platforms were launched to connect scientists who had embarrassingly parallel computational workloads with volunteers all around the world. Volunteers offered their own computers for free in exchange for participating in large scientific projects and competing on leaderboards for non-transferable Web2 points. In the case of SETI@home, the computations helped the search for extraterrestrial life. In the case of Folding@home, the computations were related to protein dynamics. Out of SETI@home grew BOINC, the Berkeley Open Infrastructure for Network Computing, which generalized the project creation and job generation mechanisms to other scientific computing projects, of which today there are tens in existence and operating. The lesson from BOINC is clear: people will contribute compute for free if they care about the project. They'll download software, configure their machines, and donate cycles in exchange for leaderboard points and the satisfaction of contributing to science. No money changed hands - just participation in something larger than themselves. This model worked. Millions of computers contributed billions of hours of compute time. But it hits an upper limit: participation saturates once you've reached the set of people willing to donate for free, and even at its peak it covers only a small fraction of the idle compute that exists globally. If you want to unlock orders of magnitude more capacity, especially for work that doesn't have a built-in volunteer audience, you need incentives that look like markets, not charity. ## The value capture problem Volunteer compute can power serious science: papers, datasets, and occasionally downstream intellectual property. Contributors might show up in acknowledgements or consortium-style authorship, but the compensation is rarely economic. With proper tracking and incentives, those that contributed computational resources could have received a portion of downstream value. In practice, they generally don't. The compute providers get points on a leaderboard and, in some cases, acknowledgements or consortium-style authorship. The value they help create still mostly accrues elsewhere: institutions, companies, and shareholders. The infrastructure that enabled the discovery isn't part of the value capture. This isn't a complaint about those specific projects. They did exactly what they were designed to do: harness volunteer compute for scientific problems. The problem is the design. When you can't track contributions in a way that enables later compensation, you can't create markets for speculative work. You're limited to charity. ## Futures markets for compute These new markets aim to answer the following question: are there computations for which nobody is willing to pay now, but that somebody might be willing to pay for later? There are two primary approaches. First is real-time tokenization: an application-specific token that contributors earn as they provide compute. Because the token trades in open markets, contributors can sell into stablecoins to cover near-term costs like electricity and hardware. The market price becomes an imperfect but useful signal of expected future value. People can speculate on which applications will produce real outcomes. That speculation isn't just commentary: it finances compute. Second is retroactive rewards: keep an auditable record of who contributed what, and pay out later if value materializes. This ties compensation to real outcomes and avoids forcing early speculation into a single price signal. The tradeoff is timing: retroactive rewards do not directly solve cash flow unless contributors can borrow against expected future payouts. In practice, the two can be combined. A liquid token can be a tradable claim on future retroactive rewards, with the record as the source of truth for who earned what. At a high level: 1. An application defines a claim on future value and the rules for later compensation. 2. Contributors who provide compute receive claims in proportion to their contributions. 3. Those claims trade in an application-specific market, so contributors can realize value early and others can fund work they believe will pay off. 4. If real value materializes, claimholders can be compensated through retroactive rewards, licensing revenue, or whatever payout the application specified up front. If the application produces nothing valuable, the claims are worthless. If it leads to a breakthrough, claimholders share in the upside. This is not purely theoretical. In the early days of cryptocurrencies, a number of protocols attempted to apply Bitcoin's reward mechanism to scientific computing. Gridcoin, Curecoin, Pinkcoin, and Primecoin were among these early science-focused computing coins. However, blockchains were in their infancy at the time, and while these cryptocurrencies accomplished much, they fell short of their ultimate visions. The infrastructure has matured since then. Arkhai's native tracking of jobs and their artifacts can be paired with retroactive reward mechanisms. A verifiable record of what was computed, by whom, and under what conditions enables arbitrary reward structures over those computations. ## The immutable record Retroactive rewards require attribution. Attribution requires a record. Computational reproducibility facilitates a degree of trust among network participants that computations performed using the protocol have been done correctly, since negative behavior comes with economic consequences. A scientific record of which nodes requested the computations, which nodes performed them, and what the inputs and outputs were enables incentive mechanisms that can reward (or penalize) different parts of the scientific process. This record is the foundation for retroactive rewards. When an application succeeds, the reward mechanism can look back at who contributed what. The trail is immutable. Attribution is verifiable. Payment can flow back to contributors proportionally. ## The privacy tension There's a complication. The creation of intellectual property is complicated by the fact that storing all of the inputs and outputs of jobs publicly may be undesirable for some forms of intellectual property. A drug discovery computation might produce proprietary molecules. A climate model might use licensed satellite data. A machine learning training run might involve confidential datasets. Making everything public isn't always legal, and it isn't always desirable. One option is to allow anything that can be public to be public and make the rest privacy-preserving using secure enclaves, with verifiable credit attribution to the relevant contributors. The secure enclave runs the computation. The inputs and outputs stay confidential. But the enclave produces an attestation: this node contributed this much compute to this job. The attestation is public even when the data isn't. Credit attribution works, retroactive rewards work, and the actual intellectual property remains protected. This tension between transparency and privacy runs through everything we're building. The market needs transparency to function: prices, contributions, outcomes. The applications need privacy to exist: confidential data, proprietary methods, competitive advantage. The architecture has to support both. ## What this means for you If you're running large-scale scientific computations, application-specific markets provide an alternative to grants, university clusters, or volunteer compute. Contributors can receive tradable claims that gain value if the work succeeds. This changes the incentive structure: you're not asking for charity, you're offering participation in potential upside. If you're building applications that generate intellectual property, the hybrid approach (public attribution, private data) lets you participate in decentralized compute markets without exposing your competitive advantage. If you're thinking about retroactive rewards, Arkhai's immutable record provides the attribution layer. When it's time to fund past contributions, the record exists to determine who contributed what. ## Next in the series We've covered the full stack: primitives, verification, collateral, adversarial design, and now the economics of idle compute. In our next post, we'll zoom out to the vision level. What does it look like when compute, energy, storage, and bandwidth all trade on the same infrastructure? What emerges from compositional game theory applied to market design? --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## The Generic Marketplace Vision Eighteen Months and a Million Dollars: Part 8 Excerpts from the Arkhai Whitepaper > **Historical research context — February 2026** > > This post preserves publication-time research and product-direction framing from before the May 2026 SCM component release. Its scenarios, market categories, production language, and generalized negotiation are not claims of current SCM behavior, deployed infrastructure, live supply, provider participation, customer adoption, or production readiness. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > - Compute alone isn't enough: a marketplace for compute, storage, and bandwidth together is worth far more > - Inspired by compositional game theory: treat mechanisms as composable building blocks, with explicit interfaces > - Decentralized vehicle routing: in some settings, coalition formation + credible commitments can push the Price of Anarchy toward 1 > - Coalition formation with carbon optimization: minimize emissions while completing all compute jobs > - Agents will create their own languages when negotiating. Synthetic assets will emerge as agents encounter limitations of real-world assets. > - The trajectory: multi-agent systems operating in multi-token economies, a self-improving cybernetic system guided by market forces ## Beyond compute In earlier posts, we said the same primitives that enable compute markets also enable other markets, like energy, storage, bandwidth, information, and real-world assets. We've spent the past several posts building up those primitives: Alkahest for exchange and commitment, verification for trust, collateral markets for economic guarantees, adversarial design for robustness, and tokenization for idle compute. Now let's talk about what happens when markets start interacting with each other. A marketplace for compute is not worth nearly as much as a marketplace for compute, storage, and bandwidth. The ability to have a job computed on one node, the corresponding result sent to a second node, and then stored on the second node for some period of time with proper incentives for each of the nodes involved is still quite difficult to do with most current distributed protocols, despite the relative simplicity of the task. This difficulty is a consequence of fragmented design. Protocols that focus on compute expect developers to integrate with separate storage protocols and separate bandwidth solutions. Each comes with its own tokens, its own APIs, and its own trust models. The complexity barrier blocks adoption. The integrated approach is different. Same infrastructure for compute, storage, bandwidth, and beyond. Same escrow contracts. Same collateral patterns. Same verification framework. Different validators and conditions for different asset types, but the same underlying architecture. In this post, we'll sketch one line of theory that influenced how we think about this (compositional game theory), then walk through two examples, and then end with what we expect agents to create as these markets become composable. ## Compositional game theory (as inspiration) One source of inspiration is compositional game theory: a framework that applies category-theoretic concepts to game theory by composing games with explicit interfaces. Traditional mechanism design treats each mechanism as a monolithic system requiring complete analysis from scratch. This limits complexity: designing a new mechanism means starting over. It limits reuse: proven mechanisms can't be easily combined with others. Compositional game theory treats mechanisms as building blocks. Open games have defined inputs, outputs, and interfaces. Composition operators let you connect games together in a few ways: sequentially (outputs feed into inputs) and in parallel (games interact side-by-side), so the overall system is assembled from smaller pieces. One useful consequence is that analysis can sometimes become compositional too: under the assumptions of the formalism, you can reason about parts and then lift those results to the whole. We're not claiming compositional game theory "enables" every marketplace we care about. It's a perspective: define primitives with clean interfaces, then build complex mechanisms by composition, and validate the composite system with the right mixture of analysis and empirical testing. ## Decentralized vehicle routing Here's a concrete coordination example. It's less about "compositional markets" and more about coalition formation plus credible commitments: agents making enforceable agreements that reshape collective outcomes. Imagine overlooking a city from above. Every agent is trying to get from point A to point B on a train, in a car, on a bike, or walking. Each agent accesses their favorite map application, is returned the shortest path, and follows it. Now imagine overlooking the same city, but the agents can be coordinated via an optimization algorithm that minimizes the average travel time. The ratio of the average travel time in the selfish case to the optimized case is called the Price of Anarchy. In stylized settings, if agents can form coalitions and use credible commitments to enforce agreements, the toolset exists to try to push the Price of Anarchy toward one. The mechanism is intuitive. If my taking a different route makes your trip faster, you can pay me to do it. But for that agreement to work at scale, it has to be enforceable: payments need escrow, conditions need validators, and disputes need resolution. That's exactly the shape of a credible-commitment system. The benefits are concrete: lower air pollution, less mental health stress from traffic, lower emissions, and higher productivity. The point isn't that vehicle routing is "a marketplace" in the narrow sense. It's that many collective coordination problems become tractable once agents can make, price, and enforce commitments. This isn't a compute problem. It's a coordination problem solved through market mechanisms. The same primitives handle both. ## Coalition formation with energy Compute nodes forming coalitions can be modeled as a game where nodes gain some benefit (higher expected return, lower variance in returns) by joining together. These nodes might also want to incorporate energy into their decision-making. Consider the objective function: minimize carbon emissions subject to all compute jobs getting done and all homes getting the energy they require. This needs consensus-based load balancing across compute providers, energy suppliers, and consumer demand. Coalition formation depends on each node's resources, the resources of prospective coalition members, and the resources of other nodes in the environment. Nodes need to communicate with each other and have a sense of their expected payoff. The reinforcement learning primitives enable them to learn from their environments. Multi-agent debate, an emerging trend in AI, extends this to LLMs. The literature on resource- and skill-based games has focused on traditional scenarios: corporate environments where workers and departments have different resources and skill sets. The literature can be extended to considering LLMs as resources, differentiated by their architectures, weights, and access to information. ## What agents create The future is autonomous. Most economic activity in the future will be undertaken by machines, yet most of our marketplaces are built for humans. The permissionless aspect of blockchain-based markets will dramatically expand the user base, and autonomous agents will be the primary users. When agents are the users, the artifacts of markets change. First, negotiation becomes native. Agents will negotiate with each other on behalf of human owners and institutions, and increasingly for their own objectives as they manage infrastructure directly. For many real-world assets, that negotiation will look like fixed schemas and protocols (JSON messages, typed offers, explicit constraints). For intents-based deals, language-model-based agents will negotiate in richer spaces that aren't fixed ahead of time. Second, communication compresses. New languages emerging from agent interactions might sound strange, but it's already happened in controlled settings: when neural networks are trained to communicate efficiently with each other, they develop protocols that optimize for bandwidth and coordination, not human interpretability. Third, assets generalize. Synthetic assets will emerge as agents negotiate over real-world assets and encounter their limitations, just as humans did. Derivatives, futures, and options emerged to manage risk and express complex economic intentions. Agents facing the same constraints will create their own abstractions. The trajectory of blockchains and cryptocurrencies is trending towards multi-agent systems operating in multi-token economies. It is not only the tokenization of real-world assets that pushes this trend, but on a larger scale, the separation of concerns facilitated by having multiple tokens representing different asset types. ## The cybernetic system Generalized marketplaces provide agents with the substrate on which to trade the very physical components that constitute them. The reinforcement learning primitives discussed in our first post provide agents with an understanding of what they are and what their role is. Combining this self-awareness and the ability to engage in economic interactions over the physical components they are composed of provides a foundation for a decentralized collective machine intelligence which can guide itself through market forces towards a self-improving cybernetic system. This is the vision: not a single AI, but a distributed system of agents coordinating through markets. Each agent optimizes its own utility. The market aggregates those individual optimizations into collective behavior. The system improves as agents learn, as markets mature, and as better mechanisms are discovered. Out of these multi-agent systems will emerge collective intelligence capable of achieving far more than is possible with current technologies. The steady improvements in AI models, computational power, and networking are such that humanity is on the cusp of decades of theory stowed away in AI and game theory journals being rapidly implemented in practice. ## What this means for you If you're building applications that touch multiple resource types (compute + storage, bandwidth + latency, energy + carbon), the integrated architecture eliminates the integration burden. One set of contracts, one collateral framework, one verification layer. If you're researching multi-agent coordination, compositional game theory is a useful lens for modular mechanism design: define interfaces, compose parts, and be explicit about assumptions. If you're thinking about the long term, the trajectory is clear: more agents, more markets, more composition. The infrastructure being built now determines what becomes possible later. ## Next in the series We've covered the full vision: from primitives to verification to adversarial design to economics to market composition. In our final post, we'll bring it back to the practical: what can you build today? --- *Arkhai is building machine-actionable marketplace infrastructure. If you're working on problems that intersect with compute markets, agent coordination, or decentralized infrastructure, we'd like to hear from you.* --- ## What You Can Build Today Eighteen Months and a Million Dollars: Part 9 Excerpts from the Arkhai Whitepaper > **Key Takeaways** > > - The original whitepaper was completed in summer 2024; the public product surface has evolved since then > - Settlement primitive: Alkahest (escrow + arbitration) plus developer SDKs > - Alkahest expresses peer-to-peer agreements as Statements, validators/arbiters, and escrowed value > - Git Commit Marketplace: pay the first commit that passes your tests > - Agentic RAG: provenance-first RAG pipelines for scientific work > - Generalized marketplace protocol: at publication, this was described as soon to be released; SCM was released later > **Historical context — March 2026 pre-SCM-release snapshot** > > “Today,” “right now,” and availability statements in this post refer to March 2, 2026. SCM was released later, and the broader compute-and-energy and arbitrary-constraint negotiation language below remains research direction rather than current SCM behavior. Current SCM VM rounds vary scalar price while non-price VM terms remain fixed or validated. See [Arkhai Compute](https://compute.arkhai.io/) and the [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. ## Eighteen months and a million dollars The whitepaper was a thesis about machine-native markets. The last eighteen months have been about turning one part of that thesis into something real: settlement in open environments. A bit changed in the process (but surprisingly, not much!). Some ideas stayed as research. Some became experiments. Many became concrete interfaces you can use today. Below are the application surfaces the post described as existing at its March 2026 publication date, written in the same format: what it is, who it's for, and what you get. ## Application 1: Escrow and arbitration (Alkahest) Alkahest is Arkhai's open-source escrow and arbitration system for peer-to-peer agreements. You describe what should happen, under which conditions, and who decides when those conditions hold. Escrow enforces the outcome. At the interface level, this is a small set of primitives: - **Statements** to represent obligations - **Validators/Arbiters** to evaluate conditions (and escalate when necessary) - **Escrow obligations** to hold and release value Who this is for: - Anyone building marketplaces, bounties, or escrowed services - Teams that need conditional payment in open environments (not just trusted counterparties) - Protocol designers who want a reusable settlement layer instead of bespoke per-app escrow What you get: - A programmable escrow interface you can reuse across deal types - Dispute resolution and recursive escalation when automation runs out - Composable conditions (boolean logic over on-chain and off-chain signals) ## Application 2: Markets for shipped code (Git Commit Marketplace) Git Commit Marketplace turns commits into tradeable units of work: escrow funds, define verification (tests), and pay the first commit that satisfies the objective. Who this is for: - Open source maintainers funding specific changes - Teams that want outcome-based contracting instead of hourly workflows - Organizations that want transparent pricing for discrete engineering tasks What you get: - Faster, more legible procurement of software work - Automated verification as the oracle for payment - Settlement that doesn't require trusting the counterparty ## Application 3: Provenance-first research RAG (Agentic RAG) Agentic RAG is an open-source approach to retrieval-augmented generation for scientific and technical work, focused on reproducibility and provenance rather than chatbot answers. Who this is for: - Researchers and labs building literature maps and research assistants - Teams that need traceable, reproducible retrieval pipelines - Organizations building internal knowledge systems that must be auditable What you get: - RAG pipelines that preserve provenance from source → chunk → embedding → retrieval → output - A system designed for inspection and iteration, not black-box responses - A way to turn retrieval into a first-class, reusable artifact ## Application 4: Compute and energy markets driven by cybernetic agents At publication, this section described a pre-release direction for generalized compute-and-energy markets: autonomous agents negotiating over cycles and watts under constraints such as latency, carbon, and reliability. Current SCM implements a narrower compute path whose VM rounds vary scalar price while resource, duration, provisioning, and non-amount escrow terms stay fixed or validated. Energy markets and arbitrary-constraint negotiation remain direction, not current SCM behavior. Who this is for: - Compute and energy providers exploring market-based allocation - Data center operators looking to maximize their ROI on compute and energy infrastructure - Compute purchasers looking for the best deals given their requirements - DePIN and infrastructure teams that need programmable settlement primitives - Builders who want to participate in shaping the threat model and mechanism choices early What you get: - A path to agent-mediated resource allocation built on a concrete settlement layer - Early integration points: settle deals with escrow/arbitration, then iterate on matching and optimization ## Getting started Pick one surface and build one small loop end-to-end: - **Alkahest (escrow + arbitration)**: Model your deal as a Statement. Decide what evidence releases funds, and who arbitrates when evidence is ambiguous. Start with one validator and one outcome, then add complexity. - **Git Commit Marketplace**: Choose an objective (a failing test, a benchmark, a feature flag). Escrow funds. Pay the first passing commit. Start with deterministic checks, then expand to richer verification. - **Agentic RAG**: Make provenance a first-class output. Persist source references, chunk IDs, and retrieval traces so results can be audited and reproduced. - **March 2026 generalized-market direction**: The post proposed defining deal formats and constraints such as latency, carbon, and reliability, then iterating on negotiation and matching above Alkahest. This was not current SCM behavior. --- _For current compute infrastructure and limitations, explore [Arkhai Compute](https://compute.arkhai.io/) or inspect the [SCM source](https://github.com/arkhai-io/simple-compute-market)._ --- ## Simple Compute Market Public Release Simple Compute Market Series: Part 1 of 6 > **Historical release context — May 29, 2026 component release** > > This post describes the `market-cli-v0.5.3` component release and its publication-time framing. Current `main` is later and is covered by the repository's root MIT license; the component tag predates that root license. Neither the release nor current source proves an Arkhai-hosted market or live supply. See [Arkhai Compute](https://compute.arkhai.io/) and [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > > - The Simple Compute Market (SCM) public open-beta code release is available for evaluation > - Buyers, sellers, and indexers get a machine-readable path through discovery, negotiation, escrow-backed commitments, provisioning, lease state, and settlement > - CLI-based with configured operator registries and no single mandatory matching broker; access can still be operator-gated > - Settlement runs through Alkahest escrow, and the release provisions KVM VMs with SSH credential handoff and optional GPU passthrough > - The release had no SCM-native token; current `main` is covered by the root MIT license, and current Arkhai SCM fee boundaries are documented separately below The Simple Compute Market (SCM) public open-beta code release is available for evaluation. SCM software implements infrastructure for agent-driven compute markets. Buyer software can discover across configured registries, negotiate scalar price under policy, lock escrow, receive seller-operated access, and settle without a human driving every step. The release connected a bounded, machine-readable compute agreement to an onchain settlement commitment; it did not implement arbitrary-term exchange. Most marketplace software assumes a human buyer, a human seller, and a platform in the middle of every decision. SCM starts from the opposite assumption. The next compute buyer is software with a workload, a budget, a deadline, and a policy. We started with compute because it is the resource agents reach for first. The public open-beta code release gives buyers, sellers, and indexers a machine-readable path through the compute-market loop: discovery, negotiation, escrow-backed commitments, provisioning, lease state, and settlement. SCM is CLI-based for buyers and sellers. Indexers run operator-configured registry APIs that sellers publish to and buyers query. SCM does not require a single mandatory matching broker, but deployments can still have registry operators, access policies, governance, and optional services. The release had no SCM-native token. SCM's current `main` source is covered by the repository's later root MIT license; the May 29 component tag predates that license. Arkhai currently charges no SCM platform fee, while sellers, infrastructure providers, networks, settlement rails, operators, and services can still impose costs. Zero protocol fees are intended direction, not guaranteed economics. Registry access may be open or gated under an operator's policy. ## What Launched The public open-beta code release includes the basic roles and runtime surface for an agent-driven compute market: - **Buyers** run `market` to discover listings, negotiate with seller storefronts, lock escrow, receive access, and track lease state. - **Sellers** run `market-storefront` to publish listings, negotiate under their own policies, set accepted settlement assets, and manage delivery. - **Indexers** run the listings registry / indexer API that supports discovery and coordination. - **Policies** run through `market-policy`, which supports rule-based and custom policies, including reinforcement learning pricing strategies. Signing provenance is Ethereum-native in this release. Participants authenticate with wallet signatures, which identifies the signing publisher but does not verify legal identity, KYB, service quality, or capacity. The identity layer is pluggable, so operators can separately integrate another scheme. ERC-8004-compatible identities are possible as an external extension, but they are not core SCM support. Negotiation is buyer-driven and policy-mediated. Buyers send signed HTTP requests to seller storefronts. Seller policies can quote, counter, accept, or walk away. In the current VM flow, rounds vary scalar price while resource, duration, provisioning, and non-amount escrow terms remain fixed or validated. Settlement runs through Alkahest escrow. SCM examples center ERC-20 flows today, while Alkahest can settle ERC-20, ERC-721, ERC-1155, native-token, and bundled-token obligations. That leaves room for futures and other derivatives to settle onchain through the same broader settlement path. The public open-beta code release provisions KVM virtual machines with SSH credential handoff and a full lease lifecycle, with optional GPU passthrough. Other resource adapters require separate implementation and confirmation. `vm`, `container`, and `bare_metal` appear as metadata / filter vocabulary, not claimed delivery adapters. Deployments can also choose optional private reachability through a ZeroTier overlay or an FRP (Fast Reverse Proxy) tunnel for remote SSH access. ## Why Compositional Game Theory SCM is inspired by Compositional Game Theory (CGT), without following CGT formalisms strictly. The reason is composability. Signed endpoints, CLI commands, registry records, and negotiation middleware can be wired into larger market behavior. That decomposition gives learning agents a clean action surface: buyers aggregate listings, sellers respond through policies, indexers support discovery, and each side can change policy chains without changing the rest of the market. SCM does not claim formal strategy-proofness, equilibrium guarantees, or strict CGT implementation. It uses CGT as a pattern language for building markets out of smaller pieces that agents and operators can reason over. ## What You Can Build A few of the things teams can build on SCM today: - **A marketplace layer for a compute network.** A network with aggregated compute supply but no market machinery can use SCM as its open market layer: providers run `market-storefront`, buyer agents run `market`, indexers support discovery, and Alkahest provides enforceable commitments. - **Idle or surplus compute monetization.** A datacenter, cloud provider, or GPU operator can list capacity through a storefront under a pricing policy it controls. Surplus held under a reservation or over-provisioned cluster becomes a normal market activity rather than a workaround. - **Agent-driven procurement.** An autonomous system or other buyer agent can run `market` to discover, negotiate, commit, and consume compute. The flow is machine-readable, and its settlement commitment is onchain; this does not make every offchain listing, availability assertion, or delivery fact independently verified. - **Custom pricing and negotiation strategies.** Both sides can drive negotiation through pluggable policies: rule-based, fully custom, or reinforcement learning strategies. ## Who It Is For - **Registry and market operators** that want open infrastructure for discovery, negotiation, and settlement without a single imposed platform model. - **Compute sellers and resource operators** that want control over seller-originated listings and rates, negotiation policy, infrastructure, provisioning, and delivery within an operator-configured settlement environment. - **Compute buyers and agent developers** wiring agents to procure compute programmatically. - **Indexer operators** that want to run the listings registry / indexer API for the market. - **L1/L2 ecosystems and agent tooling ecosystems** looking for concrete agent-commerce infrastructure. ## What To Try - [Browse the code](https://github.com/arkhai-io/simple-compute-market) - [Run the buyer quickstart](https://github.com/arkhai-io/simple-compute-market/blob/main/docs/buyer-quickstart.md) - [Stand up a storefront and list capacity](https://github.com/arkhai-io/simple-compute-market/blob/main/docs/seller-quickstart.md) - Write a negotiation policy through `market-policy` - [Scope adapter, deployment, or integration needs](https://www.arkhai.io/contact) The public open-beta code release is market infrastructure, not a hosted compute catalog operated by Arkhai. Source and release evidence do not establish any live listing, available capacity, or participating provider; the current website fails closed without an approved source and labels any availability as seller-asserted. ## Evaluate the Release Providers, buyers, and indexer operators can evaluate the release through the role quickstarts. - [Explore Arkhai Compute](https://compute.arkhai.io/) - [Explore the code](https://github.com/arkhai-io/simple-compute-market) - [Request a Market](https://compute.arkhai.io/request-market) - [Alkahest](https://github.com/arkhai-io/alkahest) - [Compositional Game Theory docs](https://www.arkhai.io/docs/modular-markets) - [Compositional Game Theory GitHub](https://github.com/arkhai-io/cgt/tree/main/cgt-docusaurus/docs) - [Whitepaper](https://www.arkhai.io/whitepaper) --- ## Simple Compute Market Architecture Simple Compute Market Series > **Historical release context — May 29, 2026 component release** > > This post discusses the May 29 `market-cli-v0.5.3` component release and its publication-time framing. Current `main` is later and is covered by the repository's root MIT license; the component tag predates that root license. Neither the release nor current source proves an Arkhai-hosted market or live supply. See [Arkhai Compute](https://compute.arkhai.io/) and [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > > - SCM's architecture is a set of separable services and roles, not a single centralized marketplace > - Three roles carry the market and each can run on its own: buyers (`market`), sellers (`market-storefront`), and indexers (the listings registry) > - A shared service layer handles chain, Alkahest, and registry access; wallet identity is signing provenance, not legal identity or KYB > - Core design choices: bilateral scalar-price negotiation over signed HTTP, a pluggable policy engine, Alkahest reuse for settlement, and configured registry discovery > - The shipped delivery path provisions KVM VMs with SSH handoff and optional GPU passthrough; other resource adapters require separate implementation and confirmation Most marketplaces keep their machinery inside one platform: discovery, pricing, payment, delivery, and disputes all run through one operator, behind one account. SCM is built the other way. Its public open-beta component release supplied software for agent-driven compute markets, and its architecture is a set of separable services and roles, not a centralized marketplace. The later current `main` source is covered by the root MIT license. The launch post introduced the release; this post opens it up. Discovery runs through one or more configured operator registries rather than one mandatory platform-owned broker. Registry access can still be gated by operator policy. The public open-beta code release provisions KVM virtual machines with SSH credential handoff and optional GPU passthrough. Other resource adapters require separate implementation and confirmation. ## Separable Roles, Not One Platform Three roles carry the market, and each can run on its own: - **Buyers** run the `market` CLI: discover listings, negotiate, lock escrow, receive credentials, and track lease and recovery state. The buyer is a pure HTTP client with no server. - **Sellers** run `market-storefront`: publish offers, answer negotiations under their own policy, integrate provisioning, and report delivery and settlement state. - **Indexers** run the listings registry: a discovery and coordination surface. Underneath sits a shared service layer the buyer and storefront both use: a chain client, an Alkahest client, and a registry client. Signing provenance is Ethereum-native, carried as generic `(scheme, identifier)` references so the surface stays open to other schemes; it does not establish legal identity, KYB, quality, or capacity. Settlement runs through Alkahest escrow, with arbiters and claim, refund, reclaim, and recovery paths. Reachability can use ordinary endpoints or an optional ZeroTier overlay for private reachability. And the negotiation layer takes pluggable policies, including a Puffer-trained reinforcement learning pricing example as one option. ## The Design Choices Behind It A few decisions shape the whole system: - **Bilateral negotiation, not a central order book.** Buyers and sellers exchange signed offers and counteroffers peer-to-peer over HTTP, rather than through a mandatory matching engine. The current VM flow varies scalar price while resource, duration, provisioning, and non-amount escrow terms remain fixed or validated. - **A policy engine as the extension point.** Pricing strategy is pluggable: deterministic, custom, or learned. The selected schema and policies validate payment requirements and accepted settlement assets. - **Alkahest reuse, not custom escrow.** Settlement inherits crypto-native assets, arbiter criteria, and release paths instead of hard-coding one platform payment model. Current examples center ERC-20; Alkahest itself can settle ERC-20, ERC-721, ERC-1155, native-token, and bundled obligations. - **Configured discovery, not one mandatory platform-owned broker.** Buyers can query one or more configured operator registries; access may be open or gated. Wallet signing supplies publisher provenance, and ERC-8004-compatible identities are available only as an external extension. - **A concrete delivery path first.** KVM VM provisioning with SSH handoff and optional GPU passthrough is the shipped path; other resource adapters require separate implementation and confirmation. - **A pattern language, not a monolith.** Inspired by Compositional Game Theory: markets composed from smaller pieces agents and operators can reason over. ## What The Architecture Enables Because the roles are separable, different participants can do different things with the same system: - Buyers can query storefronts and exchange policy-governed offers and counteroffers programmatically. - Sellers can expose KVM VM capacity, with optional GPU passthrough, through their own listings, policy, and settlement requirements. - Indexers can run the listings registry for discovery and coordination. - Registry and market operators can evaluate separable services in self-hosted or operator-owned deployments without being locked into one provider. - Evaluation teams can identify where a delivery adapter, policy, or deployment would need scoped work; this is not evidence of current partners or deployments. - Researchers and agent developers can read a concrete, end-to-end agent-commerce system. ## Inspect The Architecture - [Explore the repository](https://github.com/arkhai-io/simple-compute-market) - [Read the architecture reference](https://github.com/arkhai-io/simple-compute-market/blob/648682c67a26caf9283492e999923ab6ca1206ee/docs/development/ARCHITECTURE.md) - [Explore Arkhai Compute](https://compute.arkhai.io/) - [Scope a deployment or adapter](https://www.arkhai.io/contact) Separable roles. Direct scalar-price negotiation. Explicit commitments. No single service has to own the market. Next, the buyer's path: from a workload to a provisioned machine. --- ## Simple Compute Market For Compute Buyers Simple Compute Market Series > **Historical release context — May 29, 2026 component release** > > This post discusses the May 29 `market-cli-v0.5.3` component release and its publication-time framing. Current `main` is later and is covered by the repository's root MIT license; the component tag predates that root license. Neither the release nor current source proves an Arkhai-hosted market or live supply. See [Arkhai Compute](https://compute.arkhai.io/) and [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > > - SCM gives buyers a software-driven path through configured registries: discover, negotiate scalar price, escrow, receive, and settle > - Agent procurement needs machine-readable evidence and actions rather than browser-only dashboards and manual clicks > - Beyond machine-readable APIs, SCM adds crypto-native settlement, policy-governed price negotiation, deployment-set settlement assets, and arbiter and release criteria > - Buyer software drives buyer-side decisions through discover, evaluate, negotiate, commit, receive, track, settle, and recover; the seller still provisions and delivers > - Listings retain publisher provenance, seller-asserted availability, and separate displayed and final terms; provenance is not verified legal identity or KYB > - The public open-beta code release provisions KVM VMs with SSH access and optional GPU passthrough; other delivery adapters require separate implementation and confirmation Your software can already schedule a workload. Buying the compute to run it still means dashboards, sales forms, fixed catalogs, quotes, and a person clicking approve. That model does not fit an agent. SCM gives buyers a path through one or more configured operator registries: discover publisher-attributed listings, negotiate scalar price under policy, lock escrow, receive access, and settle through software. Registry access can be open or gated, and operators and optional services can still coordinate a deployment; SCM does not require one mandatory platform-owned broker. The launch post introduced the release; this is the same software from the buyer's seat. The public open-beta code release has a concrete delivery path: KVM virtual machines with SSH access and optional GPU passthrough. Other delivery adapters require separate implementation and confirmation. ## What Agent Procurement Needs Human buying assumes a human in the loop: a dashboard to read, a form to fill, a quote to wait on, an approval to click. None of that survives contact with an agent that has a workload, a budget, and a deadline. An agent needs the market evidence to be inspectable. Listings retain signing-publisher and source provenance, but that is not verified legal identity or KYB. Availability is seller-asserted rather than independently verified real-time capacity. Displayed terms and final negotiated terms remain distinct; commitments, delivery state, and the settlement path must be readable without implying that every assertion is verified. Plenty of compute APIs are already machine-readable. What SCM adds is crypto-native settlement, policy-governed price negotiation, settlement assets that can vary by deployment, and arbiter and release criteria a deployment can set for itself. It also means a registry or market operator does not rebuild escrow, recovery, and settlement from scratch for every new deployment. Those paths come with the buyer flow. ## The Buyer Flow The buyer's path through SCM is a sequence software can drive on the buyer side; sellers remain responsible for infrastructure, provisioning, credentials handoff, and delivery: - **Discover.** Query one or more configured operator registries for listings. - **Evaluate.** Weigh the offers against what the workload needs. - **Negotiate.** Talk to a seller's storefront over signed HTTP and a negotiation policy. The current VM flow negotiates price; duration, resource, and escrow fields are fixed or validated by the selected schema and policies. - **Commit.** Lock escrow through Alkahest once terms are accepted. - **Receive.** Take VM access through SSH credential handoff. - **Track.** Follow lease status and delivery evidence. - **Settle and recover.** Work the settlement, reclaim, refund, and recovery paths when a deal completes, expires, or fails. Buyer-side choices remain under buyer policy. Sellers still provision and deliver, while operators configure registries, access, and settlement. No single mandatory broker must hold the relationship, set a universal price, or custody the funds. ## What To Try A buyer evaluating SCM can start small: - Inspect the `market` buyer runtime in the repository. - Follow the buyer quickstart. - Run or read a negotiation against a `market-storefront`. - Decide whether KVM VMs, with optional GPU passthrough, fit the first workload, or whether another compute adapter should be scoped separately. - Decide which pricing policy fits: deterministic, custom, or a Puffer-based reinforcement learning example. ## Try The Buyer Flow - [Explore the repository](https://github.com/arkhai-io/simple-compute-market) - [Follow the buyer quickstart](https://github.com/arkhai-io/simple-compute-market/blob/main/docs/buyer-quickstart.md) - [Request a Market](https://compute.arkhai.io/request-market) - [Scope a buyer evaluation](https://www.arkhai.io/contact) Find compute. Agree on terms. Make a credible commitment. Receive the machine. Give software a workload. Let it procure compute. Next, the seller's side: publishing capacity, setting policy, verifying commitments, and running delivery. --- ## Simple Compute Market For Compute Sellers Simple Compute Market Series > **Historical release context — May 29, 2026 component release** > > This post discusses the May 29 `market-cli-v0.5.3` component release and its publication-time framing. Current `main` is later and is covered by the repository's root MIT license; the component tag predates that root license. Neither the release nor current source proves an Arkhai-hosted market or live supply. See [Arkhai Compute](https://compute.arkhai.io/) and [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > > - SCM lets sellers publish capacity to one or more configured operator registries, negotiate scalar price under seller policy, and settle through Alkahest, while provisioning stays on the seller service > - The seller flow is one path: deploy, publish, set policy, settle, provision, and operate > - Sellers keep control of seller-originated rates, infrastructure, reachability, provisioning, and delivery within the settlement assets, networks, and deployment boundaries configured by registry and market operators > - Wallet signatures establish signing-publisher provenance, not verified legal identity, KYB, quality, or capacity > - Settlement is Alkahest-backed and verified on-chain before you provision > - The public open-beta code release provisions KVM VMs with SSH credential handoff and optional GPU passthrough; other delivery adapters require separate implementation and confirmation Useful compute capacity does not become a market just because it has a listings page. To sell it, you need market machinery: machine-readable offers, negotiation policy, commitments, provisioning, delivery evidence, accepted settlement assets, and release criteria. Compute networks and operators usually end up building that plumbing themselves, or renting it from a platform that then owns the customer relationship. SCM gives sellers that machinery as inspectable software. You run `market-storefront`: publish capacity to one or more configured operator registries, negotiate scalar price under your own policy, settle through Alkahest, and keep provisioning on your own service. Registry access may be open or gated. The buyer's side has its own post; this is the same workflow from the seller's seat. The public open-beta code release provisions KVM virtual machines with SSH credential handoff and optional GPU passthrough. Other delivery adapters require separate implementation and confirmation. ## The Seller's Problem A seller has capacity. What it lacks is everything around the sale. Buyers driven by software need offers they can read, prices they can negotiate under policy, a commitment they can trust, delivery they can verify, and settlement that resolves on its own. Build all of that per market and the plumbing becomes the product. Hand it to a platform and the platform owns the relationship, the pricing model, and the payment rails. SCM separates the layers: the seller runs its storefront and delivery infrastructure, while a registry or market operator configures and runs its own registry, access, settlement, topology, and operating boundaries. One party may take more than one role, but the software does not conflate them. ## The Seller Flow Standing up a storefront follows one path: - **Deploy.** Run `market-storefront` through Docker Compose, Helm and Kubernetes, or the raw runtime. - **Publish.** Establish signing-publisher provenance with a wallet signature and publish offers to one or more configured operator registries. A signature is not verified legal identity or KYB. - **Set policy.** Configure the negotiation policy so the storefront returns accept, counter, or exit decisions. The current VM flow negotiates price; the selected schema and policies validate payment and settlement fields. - **Settle.** Use Alkahest-backed escrow for accepted commitments, and verify the on-chain obligation before you provision. - **Provision.** Connect KVM VM provisioning with SSH credential handoff for the shipped delivery path. - **Operate.** Surface lease status, delivery evidence, recovery paths, and settlement state. Reachability can use ordinary public endpoints or an optional ZeroTier overlay for private reachability, depending on deployment configuration. Throughout, the split holds. SCM software coordinates the workflow; seller infrastructure delivers the machine; registry and market operators configure their deployments. No single mandatory broker is required. ## What To Try A seller or operator can evaluate SCM in steps: - Inspect the storefront runtime in the repository. - Follow the seller quickstart. - Connect a test backend for the KVM VM path. - Run a buyer negotiation against a test storefront and watch your policy decide. - Decide whether deterministic, custom, or a Puffer-trained reinforcement learning policy fits your pricing. ## Try The Seller Flow - [Explore the repository](https://github.com/arkhai-io/simple-compute-market) - [Follow the seller quickstart](https://github.com/arkhai-io/simple-compute-market/blob/main/docs/seller-quickstart.md) - [Add VM reachability with FRP](https://github.com/arkhai-io/simple-compute-market/blob/648682c67a26caf9283492e999923ab6ca1206ee/docs/seller-frp-setup.md) - [Explore the seller pathway](https://compute.arkhai.io/#sellers) - [Scope a seller deployment](https://www.arkhai.io/contact) Your infrastructure. Your policy. Your seller-operated delivery path. Define the offer. Set the policy. Confirm the commitment. Deliver the machine. Next, the design patterns underneath all of this: what belongs to the market, what belongs to participants, and what stays specific to the resource. --- ## Simple Compute Market Design Patterns Simple Compute Market Series > **Historical release context — May 29, 2026 component release** > > This post discusses the May 29 `market-cli-v0.5.3` component release and its publication-time framing. Current `main` is later and is covered by the repository's root MIT license; the component tag predates that root license. Neither the release nor current source proves an Arkhai-hosted market or live supply. See [Arkhai Compute](https://compute.arkhai.io/) and [current SCM source](https://github.com/arkhai-io/simple-compute-market) for current boundaries. > **Key Takeaways** > > - SCM splits a market into recurring parts: separable roles, configured registry discovery, pluggable scalar-price policy, an explicit settlement handoff, and a domain-specific delivery adapter > - Buyer, seller storefront, and indexer are the runtime roles; policy, provisioning, registry, and settlement remain separable services or boundaries > - A line runs through the system: behavior that holds for every listing belongs to the market core, and anything that varies by what is sold is injected from below > - The reusable part is the separation of concerns, which is what makes the structure portable across domains > - Compute is the first domain; another market keeps the structure but brings its own delivery, metering, evidence, recovery, and release criteria Start with a concrete question: what can you reuse from SCM when you build a different agent-driven market? SCM is software for compute markets. The way it is built splits a market into parts that recur, and those parts are worth naming. The launch and workflow posts showed the system running. This one pulls out the patterns. ## The Roles Are The Pattern SCM has three explicit runtime participant roles: buyer, seller storefront, and indexer. Registry and market operators configure deployments, while policy, provisioning, registry, and settlement remain separable services or boundaries. None has to own the others. Discovery runs through one or more configured operator registries, not one mandatory platform-owned search broker; registry access can still be gated. Registries index offers and coordinate discovery. Arkhai currently charges no SCM platform fee, but operators may choose deployment economics and sellers, infrastructure, networks, settlement rails, and services can still impose costs. Zero protocol fees remain intended direction rather than guaranteed economics. Negotiation runs peer-to-peer over signed request and response, mediated by pluggable policies. Current VM rounds vary scalar price while resource, duration, provisioning, and non-amount escrow fields stay fixed or validated. Commitments, crypto-native settlement, release criteria, and recovery run through Alkahest. The public open-beta code release uses KVM VMs with optional GPU passthrough as its concrete domain adapter; other adapters require separate implementation and confirmation. A Puffer-trained reinforcement learning pricing policy is one implementation path inside that frame. The useful idea is the separation itself. Each role has a clear contract, so you can change one without rewriting the others. ## What Stays Fixed, And What Varies The deeper pattern is a line drawn through the system. A behavior belongs to the market core only if it holds for every possible listing. Anything that varies by what is being sold is supplied from below, through an injected hook. That gives a clean test. The shared structure, discovery, negotiation rounds, and the settlement handoff are invariant. Message meaning, how a participant prices its next move, and valid terms are schema- and policy-defined. In the current VM flow, this extensibility does not mean arbitrary-term negotiation: signed rounds vary scalar price, while non-price VM terms remain fixed or validated. The settlement boundary makes it concrete: negotiation reduces a conversation to terms, and settlement turns terms into a commitment. Pricing decisions stay separate from custody. Changing a policy should not touch settlement, and changing the delivery adapter should not erase the commitment. The market surfaces fall out of that line: signing provenance and metadata, discovery and listing state, policy-driven scalar-price negotiation, escrow and settlement assets, the delivery adapter and its evidence, claim, refund, reclaim, recovery, and arbiter criteria, post-trade state, and the deployment's own operating-model choices. Signing provenance is not verified legal identity or KYB. Each boundary is explicit rather than hidden behind one mandatory platform. ## What Carries Over The reusable part is the separation itself: roles with clear contracts, configured registry discovery, pluggable scalar-price policy, an explicit settlement handoff, and delivery scoped to one domain. Each piece has a defined edge, so each can change without rewriting the others. That is what makes the structure portable. Compute is the first domain. Another market keeps that structure but brings its own delivery, metering, evidence, recovery, and release criteria. The patterns give you a market's shape; each domain still does its own work. ## Inspect The Patterns - [Read the architecture](https://github.com/arkhai-io/simple-compute-market/blob/648682c67a26caf9283492e999923ab6ca1206ee/docs/development/ARCHITECTURE.md) - [Explore the repository](https://github.com/arkhai-io/simple-compute-market) - [Read the CGT pattern language](https://www.arkhai.io/docs/modular-markets) - [Tell us which market you would build next](https://www.arkhai.io/contact) - [Explore Arkhai Compute](https://compute.arkhai.io/) Make the structure explicit. Keep decisions local. Keep delivery concrete. Next, the roadmap: what the public open-beta code release is meant to test, and where it goes from here. --- ## Compute Sovereignty and Programmable Markets *How participants can shape the agreements and markets they depend on* AI is redrawing the boundary of the firm. A small team can now do work once spread across entire departments. Large companies can coordinate more complex work across business units, suppliers and regions. The scale differs; the dependence does not. Every company owns only part of what it needs. The chips, the power, the capital and the routes to customers often belong to someone else. Much of what sits outside a company arrives by agreement. When a supplier reprices, a contract ends or a venue changes its rules, the speed of software stops mattering. A company can move only as fast as it can form its next workable agreement. That agreement has a cost: finding a counterparty, judging whether they are credible and settling terms both sides can carry out. The cost sets how much power the current relationship holds. If replacing a supplier takes private introductions, weeks of assessment and a bespoke negotiation, the supplier is more than a vendor. It is a dependency. If customers can be reached through only one venue, that venue decides which businesses are seen and on what terms. Compute sovereignty means that participants retain meaningful authority over the decisions that belong to them. Buyers shape how they source capacity and make commitments. Sellers shape what they offer, how they price it and how they fulfill it. Market operators shape the rules of their venues. Exercising that authority across many relationships requires infrastructure that can carry those choices into action. A programmable market is a market whose offers, negotiation policies and venue rules can be expressed in software while participants retain authority over the commitments they make. This makes compute sovereignty practical by giving participants shared infrastructure for discovering counterparties, forming agreements and developing markets without placing every relationship under one operator. ## The business behind the model Consider a company that provides AI inference. Its customers see an API. Behind that API is a chain of commitments. The company needs machines with enough memory, in a useful region, for long enough to run its models. A compute provider needs prices and commitments that justify making those machines available. The inference company combines that capacity with its models, software and operations, then sells a new service to its customers. On Monday, a customer asks the company to process a large archive by Friday. The job needs roughly 36 hours on hardware the company does not hold. Its usual supplier has nothing free that week. The company searches for continuous access through Thursday. Another supplier has the right machines, but they are committed during the day and open for ten hours each night. In a catalog, that is the end. The request matches nothing and disappears. The company's answer to its customer now rests on a supplier it could not replace. A programmable market carries the conversation further. The company knows whether its job can stop and resume. The supplier knows which nights are open. Four nights may provide enough machine time once restarts are counted, or they may not. The two sides can find out. If the numbers work, they agree on a schedule neither offered at the start. Friday remains the company's deadline to meet. The agreement gives it the capacity to try. They may also discover that no workable agreement exists. A mismatch is information. An unmet request reveals demand for an offer nobody has built. A successful negotiation reveals something more: a combination of timing, price, risk and commitment that real participants will accept. One agreement does not create a market. When similar agreements recur, however, a pattern begins to appear. Participants can encode that pattern as a new offer, a purchasing policy or an independently governed market. What began as an exception becomes infrastructure. ## From a conversation to a market A programmable market treats the agreement as something participants can develop, rather than merely select. Offers, negotiation policies and venue rules can be expressed in software. Software searches, compares and carries out the rules the participants accepted. Buyers and sellers still make the commitments. [Simple Compute Market (SCM)](https://github.com/arkhai-io/simple-compute-market) provides open-source infrastructure for programmable compute markets. Sellers run their own storefronts and publish listings to federated registries, which store, validate and expose those listings for discovery. Buyers search those registries, then negotiate prices directly with seller storefronts through SCM's peer-to-peer infrastructure. The policies SCM ships with negotiate price. Participants can replace them with policies they write themselves. Sellers can publish through several registries without surrendering their storefronts or rebuilding every offer. Buyers can apply their purchasing policies through their own tooling across different registries. Changing an intermediary does not have to mean giving up the commercial capability built around it. That portability does not remove the work of discovery. Buyers still judge reliability, hardware fit and operating conditions. Participants could organize curators or buyer groups to share assessments and maintain venues around common requirements. Each buyer still decides which assessments to trust and which agreements to accept. Another supplier still has to exist and want the deal. Open infrastructure keeps the path short when one does. SCM also separates roles that a conventional platform bundles together. Different operators can run registries, storefronts, settlement services and fulfillment systems while sharing the interfaces that let them work together. Market builders can reuse those components, then concentrate on the rules and services that make their markets different. Shared interfaces do not require shared rules. ## What a lender can read One overnight agreement is an anecdote. When the same agreement recurs, it becomes something a supplier can plan around and a lender can read. Infrastructure is built on that kind of evidence. Operators commit to equipment, power and facilities years before the last unit of demand appears. Their customers plan in months, weeks or hours. Financing works best when the gap between them holds credible offtakers, clear credit signals and agreements that can be underwritten. The inference company sits inside that gap. It can make a longer commitment when it knows what the agreement lets it do with capacity its customers do not use. It can serve more inference customers or run another workload the agreement allows. Handing the capacity itself to another party is a separate transaction. Resale or transfer depends on the supplier's terms, and the original buyer may retain obligations afterward. Clear records distinguish interest from commitment, commitment from delivery and delivery from payment. They give operators, buyers and financiers better evidence for deciding what to build, fund or purchase. Each longer commitment from a credible buyer gives a supplier more to show lenders and investors. Those records help a lender decide whether to finance capacity. A buyer faces a different question: whether that capacity can actually serve its workload. Protecting a price and securing usable capacity solve different problems. Location, timing, topology, reliability and contractual fit still determine whether capacity can serve the work. Programmable markets make offers easier to discover, terms easier to compare and agreements easier to form, or to transfer where contracts allow. As buyers and sellers find one another and commit on clear terms, compute markets can grow deeper and more liquid. In a deeper market, capacity is easier to finance, buy and sell. ## Markets people can shape Compute is where this example begins because the need is immediate and the constraints are physical. The pattern is larger. A solar farm has power at midday that the grid will not take. A lab needs access to a dataset for three months, but the only license offered lasts a year. A dozen clinics need the same privacy terms from an inference provider, and none is large enough to ask alone. Each is an agreement waiting to be formed. None appears in a catalog, because a catalog contains only what someone has already thought to offer. In a programmable market, each can begin as a negotiation. Participants can develop arrangements that recur into standing offers, then organize those offers into venues with their own rules: a registry maintained by a trade association, a buyers' group with a shared purchasing policy or a regional market connecting power and compute. Businesses can help shape what is offered, which terms can change, how participants find one another and what authority they delegate. Shared registries, storefronts, negotiation tools and settlement components give new markets a foundation, with no single platform in control of every relationship. That authority will matter more as AI expands what organizations of every size can attempt. It will let more people start companies, help established businesses coordinate more ambitious work and give infrastructure operators new ways to serve demand. The same question reaches buyers, sellers, lenders and market organizers: can they replace, renegotiate or reshape the agreements on which they depend? If every important resource and customer relationship runs through an intermediary that cannot be replaced, software has made the organization faster without making it freer. Compute sovereignty depends on more than access to a machine. It depends on whether participants can make their own decisions, form the next agreement and help shape the market around it. Programmable markets let the people who depend on a market help build it. --- Arkhai is developing an optional managed service intended to make participation easier by handling hosted access, accounts, billing and support. Participants would still retain meaningful authority within the service: buyers would shape how they source capacity and make commitments, sellers would shape their offers, prices and fulfillment, and each side would decide which agreements to accept. SCM remains open source, and buyers and sellers can use it without Arkhai. The managed service is intended to reduce the work around participating in a market without becoming the only path to it. **[Join the early-access waitlist for Arkhai's managed service.](https://compute.arkhai.io/#waitlist-signup)**