This is a spec for ATProto accounts to request and advertise (compute) resources.
This spec is for you if you run an organization and want to leverage decentralized identity to allow access to your org's compute (and in the future more) resources to your members and their workloads/agents.
The flow is: Request For Proposal -> Bid -> Accept -> Receipt
This is pre-alpha software and it has yet to have a thorough review!!! Multiple passes of refactors were done and while things work they are just to play with right now! The compute contract flow can be considered proven. However, there is a lot of security pieces involved here and they need to be gone through with a fine toothed comb before Alpha. Please consider re-writing everything using your favorite LLM etc. let's please have many implementations! :D
If you want to quickly enable folks in your organization to be able to request compute using their ATProto accounts, or Workloads/Agents/DIDs associated with their ATProto account you can use https://digitalocean.socialweb.computer
It's a hosted service that runs a bidder in the centralized cloud for you in case you don't want to run your own decentralized on on your own compute hosts to get started.
curl -fsSL https://deno.land/install.sh | sh
git clone --recursive https://github.com/publicdomainrelay/org-root-dispatcher-typescript
cd org-root-dispatcher-typescript/atproto-market
Defaults to
--policy only-me
, so make sure you associate the bidder and
requester with the same ATproto account when prompted to scan QR code / see link
to
qr.fedfork.com
in logs.
deno run -A hono-bidder/mod.ts \
--compute-provider-local
deno run -A request-vm-ssh/mod.ts
Both operators scan the QR codes from their own processes using the did-key-associator webapp at https://qr.fedfork.com .
deno run -A hono-bidder/mod.ts \
--atproto-oauth-qr \
--atproto-handle bob.bsky.social \
--compute-provider-local \
--policy tangled-vouch \
--serve-port 0 \
--no-ingress-proxy \
--firehose-mode subscriberepos
deno run -A request-vm-ssh/mod.ts \
--atproto-oauth-qr \
--atproto-handle alice.bsky.social \
--policy tangled-vouch \
--no-ingress-proxy \
--firehose-mode subscriberepos
To send secrets to the VM without putting them in cloud-init (public ATproto
data), just pass a JSON file to
--secrets
:
For example, if you wanted to send your https://cocore.dev token to a new VM you could do it like so:
# Set ATP_HANDLE and ATP_PASSWORD for goat account login or provide like so:
goat account login -u "aliceoa.bsky.social" -p "aaaa-aaaa-aaaa-aaaa"
TOKEN=$(goat account service-auth \
--lxm dev.cocore.account.createApiKey \
--aud did:web:appview.cocore.dev)
PI_CONFIG=$(curl -sS -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
'https://appview.cocore.dev/xrpc/dev.cocore.account.createApiKey' \
-d '{"name":"my-pi"}' | jq -r '{"apiKey": .secret}')
deno run -A request-vm-ssh/mod.ts \
--atproto-oauth-qr \
--atproto-handle aliceoa.bsky.social \
--no-ingress-proxy \
--firehose-mode subscriberepos \
--policy tangled-vouch \
--secrets <(jq -n \
--arg pi "${PI_CONFIG}" \
--arg provider "cocore" \
--arg model "mlx-community/Ornith-1.0-9B-4bit" \
'[
{
path: "/root/.pi/agent/cocore-config.json",
value: $pi,
},
{
"path": "/root/.pi/agent/settings.json",
"value": {
"defaultProvider": $provider,
"defaultModel": $model
}
}
]')
Here's a quick
jq
example of using env var names as arguments instead of
writing each one out (then you'd pass
--secrets /tmp/secrets.json
):
jq -n --args '[$ARGS.positional[] | {path: ("/root/secrets/" + ascii_downcase), value: env[.]}]' \
SOME_API_KEY OTHER_ENV_VAR > /tmp/secrets.json
Passing secrets this way works by leveraging Workload Identity Federation. We
thereby avoid storing anything sensitive in the public
compute.vm
record.
Each bid's config carries a
com.publicdomainrelay.temp.compute.config.wif.simple
instance which
tells the requester prior to accept what the OIDC issuer will be for the VM
and how it's
sub
(subject) claim will be formatted.
This means that if a contract fails, aka the VM disappears for whatever reason. We can turn around and accept a different bid from another provider! This loose coupling of Workload Identity Federation configuration information ensures we can even preemptively spin more than one instance of the compute we are requesting for resiliency. The following repo has an in-depth exploitation of how workload identity works: droplet-oidc-poc .
--secrets
starts an HTTP server that lives only for the contract.
It also adds a script to the VM's cloud-init
user_data
so that When the VM
starts, it makes a request to this server using a token retrieved via
token-exchange from the bidder/compute-provider's Workload Identity issuer.
Access to the secrets is then gated by an RBAC policy which ensures that the
token the VM passes to the
--secrets
HTTP server belongs to our newly spun
VM per the bid config Workload Identity Federation (WIF) record.
sequenceDiagram
autonumber
actor Alice
actor Bob
rect rgb(240, 248, 255)
Note over Alice,Bob: Request For Proposal (RFP)
Alice->>Bob: createRecord: compute.vm
(CPUs, Memory, Disk, etc.)
Alice->>Bob: createRecord: rfp
(strongRef to compute.vm)
end
rect rgb(245, 245, 245)
Note over Alice,Bob: Bid
Bob->>Alice: createRecord: bids.free (or x402)
(determine price)
Bob->>Alice: createRecord: bid
(strongRef to RFP + payment type)
end
rect rgb(240, 248, 255)
Note over Alice,Bob: Accept
Note over Alice: Payment Settlement Flow (NOP if bids.free)
Alice->>Bob: createRecord: accept
(strongRef to RFP + Bid + receipt-of-payment)
end
rect rgb(245, 245, 255)
Note over Alice,Bob: Receipt
Note over Bob: Provision Compute
Bob->>Alice: createRecord: receipt
(strongRef to RFP + Bid + Accept)
Bob->>Alice: createRecord: compute.events.vm.onNetwork
(address: public IP or proxied FQDN)
Bob->>Alice: createRecord: market.event
(strongRef to onNetwork + receipt)
end
Note over Alice: Connect to compute at/via address
user_data
, ...)
domain: "compute"
,
payload
is a strongRef to the CCRFP). The RFP is the domain-tagged envelope
bidders and indexers route on; the CCRFP is the inner VM-specific record.
The compute contract flow can be executed purely over the firehose/relays.
That's what we'll cover in this section. For more details see the
open-architecture repo's
COMPUTE_CONTRACT_FLOW_MAP.md
.
All
market.*
records use https://badge.blue signatures, not shown here for
brevity.
Alice createRecord's the type of compute she wants
$type: com.publicdomainrelay.temp.compute.vm
cpus: 2
mem: 4G
disk: 40G
network: 500G
role: my-cool-role
user_data: |
#cloud-config
runcmd:
- [sh, -c, "echo hello world"]
location:
country: USA
region: west
Alice createRecord's a Request For Proposal (
rfp
)
strongRef
erencing the
compute she wants.
$type: com.publicdomainrelay.temp.market.rfp
payload:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.compute.vm/3mm3dolfolz2c
cid: bafyrei...vm
Bob decides what he wants Alice to pay for the compute and createRecord's the payment type for his bid.
$type: com.publicdomainrelay.temp.market.bids.free
# Can also do x402 but haven't tested this in a while:
# $type: com.publicdomainrelay.temp.market.bids.x402
# cost: 0.10
# currency: USDC
# frequency: hourly
# prepay: true
# url: https://compute-contract.bob.example/receipt
Bob createRecord's a Bid (
bid
)
strongRef
erencing the compute she
wants.
$type: com.publicdomainrelay.temp.market.bid
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyrei...rfp
payload:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bids.x402/3mm4...
cid: bafyrei...x402
Alice settles the payment and gets a payment receipt according to settlement
sub-flow. If
bids.free
, then that's a NOP.
Settlement subflow hasn't been tested in a while for x402 and these docs will be updated in the future once we re-test and validate. We're focusing on orgs who want to distribute compute to their members in an ATProto native way for free first.
Alice then createRecord's an Accept (
accept
) record which wraps her
proof-of-payment receipt (only if not
bids.free
)
$type: com.publicdomainrelay.temp.market.accept
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyrei...rfp
bid:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bid/3mm4...
cid: bafyrei...bid
Finally, Bob provisions the compute and issues a Receipt ( receipt ) when he sees the Accept.
$type: com.publicdomainrelay.temp.market.receipt
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyrei...rfp
bid:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bid/3mm4...
cid: bafyrei...bid
accept:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.accept/3mlagijgoeb23
cid: bafyrei...accept
Alice then awaits the onNetwork event
$type: com.publicdomainrelay.temp.compute.events.vm.onNetwork
address: did-key-zq3shzasuf3hvonhlzf7z52hanqxknlrncr1in8njd3ymzfnl.xrpc.fedproxy.com
$type: com.publicdomainrelay.temp.market.event
payload:
cid: bafyreihp5j7tc6xbj4yqnzanwjsqfqasazqr6cbdthuwni3xdrgy2ymmeq
uri: at://did:plc:5svqtrhheairglgiiyvutzik/com.publicdomainrelay.temp.compute.events.vm.onNetwork/3mqqgq57oss26
$type: com.atproto.repo.strongRef
receipt:
cid: bafyreicpqkmnigboxueqaa2y6lvn7b5qjaiw46jb6mqgymlahpl3v5fal4
uri: at://did:plc:5svqtrhheairglgiiyvutzik/com.publicdomainrelay.temp.market.receipt/3mqqgpxvfls26
$type: com.atproto.repo.strongRef