Bare-metal seller guide
Outcome. A bare-metal storefront that publishes whole-machine SSH-access listings, grants a buyer tenant access after settlement, and reclaims that access when the lease ends.
Before you run
The examples target Base Sepolia (chain ID 84532) with test USDC and
require a physical machine the provisioning service can reach over SSH.
The source guide's git clone follows moving repository state. Immediately
after cloning and before building, select the inspected revision:
git checkout --detach 648682c67a26caf9283492e999923ab6ca1206ee
How to bring up a bare-metal storefront: publish whole-machine SSH access listings, grant a buyer SSH access after settlement, and reclaim that access when the lease ends.
The operator model is one storefront per domain. To sell VM slices and
bare-metal access for the same physical host, run a VM storefront and a
bare-metal storefront against the same provisioning/site authority. Use the
same attribute.physical_host_id in both storefront inventories so shared
host accounting can prevent double selling.
For VM slices, see seller-quickstart.md. To run
your own listing registry instead of pointing at an existing one, see
indexer-quickstart.md.
Transitional packaging note: the bare-metal domain schema lives in
domains/bare_metal, while the current runnable provisioning/site authority still lives underprovisioning/compute/service. Until the storefront package split is complete, some compose service names and commands still use the VM seller image.
Prerequisites
- Linux host with Docker +
docker composev2. - A wallet on the EVM chain you'll operate on, funded with gas plus whatever
ERC-20 you'll accept as payment. The examples use Base Sepolia + USDC at
0x036CbD53842c5426634e7929541eC2318f3dCF7e. - An RPC URL for that chain.
- A listing registry URL + write token if the registry gates writes.
- A physical machine that the provisioning service can reach over SSH.
- An SSH keypair for the provisioning container, with the public key installed on each bare-metal node and passwordless sudo enabled for the Ansible user.
1. Get the code and build
git clone https://github.com/arkhai-io/simple-compute-market.git
cd simple-compute-market
make build-seller
build-seller currently builds the shared seller images used by the
transitional VM and bare-metal paths.
2. Configure
The storefront reads /etc/arkhai/storefront.toml inside the container,
which the compose mounts from ./config.seller.toml:
agent_id = "bare_metal_seller_one"
port = 8001
base_url = "http://<YOUR_PUBLIC_IP>:8001/"
db_path = "./src/market_storefront/data/storefront/agent.db"
log_file_path = "./logs/seller.log"
admin_api_key = "<choose-a-secret>"
[wallet]
private_key = "0x<YOUR_SELLER_PRIVATE_KEY>"
ssh_public_key = "ssh-ed25519 AAAA...placeholder seller@host"
[chains.base_sepolia]
chain_id = 84532
rpc_url = "https://sepolia.base.org"
[registry]
urls = ["http://34.41.205.175/registry"]
[registry.auth]
"http://34.41.205.175/registry" = "<your-write-token>"
[provisioning]
service_url = "http://seller-provisioning:8081"
mode = "http"
[pricing]
default_min_price = "10"
default_token_address = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
default_max_duration_seconds = 86400
For co-selling with a VM storefront, use a distinct agent_id, base_url,
port, database, and wallet identity as appropriate for the bare-metal
storefront. Both storefronts can point at the same provisioning service.
3. resources.csv
Use one resources.csv row per whole machine:
resource_id,resource_type,resource_subtype,unit,value,state,min_price,token,max_duration_seconds,attribute.gpu_model,attribute.sla,attribute.region,attribute.vm_host,attribute.physical_host_id,attribute.allocation_mode,attribute.machine_id,attribute.vcpu_count,attribute.ram_gb,attribute.disk_gb,attribute.virtualization_type
whole-host-001,compute.gpu,H200,count,8,available,10,0x036CbD53842c5426634e7929541eC2318f3dCF7e,86400,H200,99.0,"California, US",,host-ca-h200-01,exclusive,bm-host-ca-h200-01,192,2048,20000,bare_metal
Important fields:
attribute.allocation_mode = exclusivemarks the row as a whole-host listing candidate.attribute.physical_host_idis the stable physical identity used for cross-domain accounting. VM slice rows for the same host must use the same value in the VM storefront inventory.attribute.machine_idis the provisioning executor's bare-metal node alias. It must match[bare_metal_nodes]in the provisioning inventory. If omitted,resource_idis used.attribute.vm_hostis for VM slices; leave it empty in bare-metal rows.valueis the total units on the physical host. For GPU hosts, this is normally the total GPU count.
4. Add the bare-metal inventory group
Edit the provisioning inventory and add each bare-metal node under
[bare_metal_nodes]:
cd domains/vms/provisioning/iac/ansible/inventory
cp hosts.example hosts
# edit hosts with your real bare-metal node(s)
[bare_metal_nodes]
bm-host-ca-h200-01 ansible_host=10.0.0.25 public_host=203.0.113.25 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519
The alias (bm-host-ca-h200-01) must match attribute.machine_id.
ansible_host is how the provisioning service reaches the node.
public_host is optional; set it when buyers should SSH to a different
address than the one Ansible uses. Rebuild after editing the baked inventory:
make build-seller
5. Choose reclaim behavior
Bare-metal grant installs the buyer's SSH public key for a tenant account.
On release, the provisioning service runs node_reclaim_access using one
of these policies:
remove_lease_keyremoves only the SSH key recorded for the lease.lock_userremoves the lease key and locks the tenant account.delete_userdeletes the tenant account and home directory.
The default is remove_lease_key. Override with provisioning config:
bare_metal_reclaim_policy: "lock_user"
or with an environment variable:
PROVISIONING_BARE_METAL_RECLAIM_POLICY=lock_user
Use delete_user only for machines where tenant home directories are
expected to be disposable.
6. Bring it up
SELLER_CONFIG_PATH="$PWD/config.seller.toml" \
SELLER_RESOURCES_CSV="$PWD/resources.csv" \
SELLER_SSH_PRIVKEY="$PWD/keys/id_ed25519" \
docker compose -f compose/seller.yml -f compose/seller.live.yml up -d
For co-selling, run the VM and bare-metal storefronts as separate deployments with separate configs and public ports, while pointing both at the same provisioning/site authority.
7. Validate the live node
Before publishing, check that the provisioning container can reach the bare-metal node:
docker compose -f compose/seller.yml -f compose/seller.live.yml exec \
seller-provisioning ansible \
-i /opt/domains/vms/provisioning/iac/ansible/inventory/hosts \
bm-host-ca-h200-01 -m ping
Then verify the host is registered and enabled in the provisioning service:
docker compose -f compose/seller.yml -f compose/seller.live.yml exec \
seller-provisioning curl -s \
-H "X-Admin-Key: <admin_api_key>" \
http://localhost:8081/api/v1/hosts/bm-host-ca-h200-01 | jq .
Bare-metal grant/reclaim refuses to queue work for a missing or disabled machine.
8. Publish and inspect
docker compose -f compose/seller.yml exec seller-storefront \
market-storefront publish --inventory /app/resources.csv
Bare-metal listing payloads have kind = "bare_metal.v1" and include
machine_id, physical_host_id, access_method = "ssh", and the advertised
duration/price constraints. Inspect local listings:
curl -s http://<YOUR_PUBLIC_IP>:8001/api/v1/listings \
| jq '.listings[] | select(.offer_resource.kind == "bare_metal.v1")'
Operational notes
- Do not use the same machine as a general-purpose operator login and a bare-metal tenant target unless the tenant account is tightly isolated.
remove_lease_keyis safest for preserving tenant data;lock_useranddelete_userare stronger cleanup actions with more operational blast radius.- Keep
physical_host_idstable across CSV edits. Changing it breaks cross-domain accounting for existing rows. - Keep
machine_idstable while leases are active. It is the executor target used for grant/reclaim. - Re-run publish after importing resource or capacity changes so stale listings close and newly available listings reopen.
Common pitfalls
attribute.machine_idmust match[bare_metal_nodes]. Wrong alias = grant/reclaim fails before Ansible runs.- Bare-metal and VM rows for one physical host must share
attribute.physical_host_id. Otherwise the site ledger cannot prevent cross-mode double selling. - Do not put VM slice and bare-metal rows in the same storefront inventory once the storefront split is complete. Run one storefront per domain and let the shared site authority coordinate host accounting.
agent_idmust be a Python identifier — no dashes.