Diff
# Introduction
CI/CD requires compute to run build, test, release etc. workloads.
In federated and decentralized environments software projects have many shared and distinct requirements for the underlying compute associted with CI/CD workloads.
Provisioning of backing compute can be made loosely coupled via Compute Contracts. Thereby enabling a wide and dynamic variety of compute, compute providers, capabilities, trust domains, and cost structures.
# Motivation
Compute requirements varry by workload. In general, Virtual Machines and containers are common forms of compute required to run CI/CD workloads.
Compute requirements varry by workload. In general, Virtual Machines and containers are common forms of compute required to run CI/CD workloads. However, many CI/CD and adjacent flows are of could be written to operate within less (or more) resource heavy environments. IssueOps flows for example might be most performantly run in lightweight WASM environments. Integration tests of compute providers themselves may require bare metal.
Tangled.org and their Spindle API and implementation are up and coming within the decentralized space for Git Version Control Hosting and CI/CD. Examples within this RFC will focus on provisioning compute for Tangled Spindle orchestrated workloads.
# Proposal
...
> https://john.leaflet.pub/3mletyxaie22o
- Alice, Bob, and Eve are on the network
- Alice wants to issue a Compute Contract Request For Proposal (CCRFP)
- Alice's CCRFP will state she wants an OpenCode instance
- Bob has plenty of builder machines
- Eve wants to know what Alice is doing
- Alice has vouched for Bob
- Alice has denounced Eve
- Alice creates a CCRFP manifest (the VM-specific payload — cpus, mem, disk,
cloud-init `user_data`, ...)
- Alice wraps her CCRFP in a top-level RFP record (`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.
- Alice makes her RFP/CCRFP pair available to the network
- Bob and Eve each issue a Compute Contract Bid (CCB) against the CCRFP
- Alice's policy engine sees that she's denounced Eve and vouched for Bob
- Alice issues a Compute Contract Bid Accept (CCBA) against Bob's CCB.
- Alice issues a x402 payment to Bob per info provided in his CCB.
- Using the CCBA AT URI and CID to the CCB's stated CCR endpoint.
- Bob issues a Compute Contract Receipt (CCR) over the CCRFP, CCB, and CCBA
- The CCR references the CCRFP, the CCB, and the CCBA.
- Bob builds to the CCRFP manifest's spec
All cross-record references use `com.atproto.repo.strongRef`
(`{$type, uri, cid}`), so the chain is content-addressed end-to-end.
## References
- https://github.com/publicdomainrelay/compute-contract-provider-relay-digitalocean
- https://github.com/publicdomainrelay/atproto-reverse-proxy
- https://github.com/publicdomainrelay/agent-atproto-typescript
## Flow
End-to-end walkthrough of the RFP → Bid → Accept → Receipt lifecycle as
expressed by the lexicons under `lexicons/com/publicdomainrelay/temp/`.
Records are shown in YAML for readability; on the wire they are JSON
records living in ATProto repositories.
## Actors
| Actor | Role |
|----------|---------------------------------------------------------------|
| Alice | Requester. Authors the **RFP** and later the **Accept**. |
| Bob | Provider. Authors **Bids** and the **Receipt**. |
| Relay | Optional broker / hook host that turns firehose commits into HTTP webhooks (e.g. airglow). |
| PDS | Each actor's ATProto Personal Data Server holding their records. |
| Firehose | `com.atproto.sync.subscribeRepos` / Jetstream — public commit stream. |
## Record cheatsheet
All records are pre-stable and live under `com.publicdomainrelay.temp.*`.
Each cross-record pointer is a `com.atproto.repo.strongRef`
(`{$type, uri, cid}`).
```mermaid
classDiagram
class RFP {
+strongRef payload // -> VM
}
class VM {
+int cpus
+string mem
+string disk
+string network
+string role
+string user_data
+Location location?
}
class Bid {
+strongRef rfp // -> RFP
+strongRef payload // -> BidsX402
+strongRef config? // -> WIFSimple
}
class BidsX402 {
+unknown cost
+string currency
+string frequency
+bool prepay
+string url
}
class WIFSimple {
+string accept_path
+string issuer_uri
+string to_issue
+string token_path
+string url_path
+string url_route
+string subject
}
class Accept {
+strongRef rfp // -> RFP
+strongRef bid // -> Bid
+strongRef payload? // domain note
}
class Receipt {
+strongRef rfp // -> RFP
+strongRef bid // -> Bid
+strongRef accept // -> Accept
+strongRef payload? // domain note
}
RFP --> VM : payload
Bid --> RFP : rfp
Bid --> BidsX402 : payload
Bid --> WIFSimple : config
Accept --> RFP : rfp
Accept --> Bid : bid
Receipt --> RFP : rfp
Receipt --> Bid : bid
Receipt --> Accept : accept
```
## State machine
```mermaid
stateDiagram-v2
[*] --> RFP_Open : Alice creates RFP + VM
RFP_Open --> Bidding : Firehose fan-out / hook fires
Bidding --> Bidding : Provider creates Bid
Bidding --> Scoring : Listen window elapses
Scoring --> Rejected : no bid passes policy
Scoring --> Accepted : Alice creates Accept
Accepted --> Settling : Bob serves the contract / collects payment
Settling --> Settled : Bob creates Receipt
Rejected --> [*]
Settled --> [*]
```
## End-to-end sequence
```mermaid
sequenceDiagram
autonumber
participant A as Alice (requester)
participant AP as Alice PDS
participant FH as Firehose
participant HK as Hook host (relay)
participant B as Bob (provider)
participant BP as Bob PDS
participant PR as Provider relay (/receipt)
participant X4 as x402 endpoint
participant VM as Provisioned VM
A->>AP: createRecord compute.vm
A->>AP: createRecord market.rfp { payload -> vm }
AP-->>FH: commit (market.rfp)
FH-->>HK: jetstream / firehose event
HK->>B: POST /hook/rfp (webhook envelope)
par bid window (N seconds)
B->>BP: createRecord bids.x402
B->>BP: createRecord config.wif.simple
B->>BP: createRecord market.bid { rfp, payload, config }
BP-->>FH: commit (market.bid)
FH-->>A: jetstream (bid for my rfp)
end
A->>A: policy filter + scorer (lowest cost)
A->>AP: createRecord market.accept { rfp, bid }
A->>X4: GET / pay (url filled with accept uri+cid)
X4->>PR: POST /receipt/{accept.uri}/{accept.cid}
PR->>BP: resolve accept -> bid -> rfp -> vm + config
PR->>VM: provision (cloud-init writes accept.json, runs role)
PR->>BP: createRecord market.receipt { rfp, bid, accept }
BP-->>A: receipt strongRef
```
## Step-by-step records
### 1. Alice publishes the VM (payload of the RFP)
```yaml
$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 > /var/log/hi"]
location:
country: USA
region: west
# rkey assigned by PDS, e.g. 3mm3dolfolz2c
# uri: at://did:plc:alice/com.publicdomainrelay.temp.compute.vm/3mm3dolfolz2c
# cid: bafyreif4toqzci4nu3thujm2quurs4h432qk3gxvmkwze2wrrznn757omi
```
### 2. Alice publishes the RFP envelope
```yaml
$type: com.publicdomainrelay.temp.market.rfp
payload:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.compute.vm/3mm3dolfolz2c
cid: bafyreif4toqzci4nu3thujm2quurs4h432qk3gxvmkwze2wrrznn757omi
# uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
# cid: bafyreib5u2krsumyya5eiqc7ys7iz3xxlourd34p7qlpehi7a7h2kdc3ia
```
### 3. Firehose → webhook envelope (airglow shape)
This is what relays/hook hosts deliver to a `/hook/rfp` style route.
```yaml
automation: at://did:plc:relay/run.airglow.automation/3mlywhsfdz222
lexicon: com.publicdomainrelay.temp.market.rfp
event:
did: did:plc:alice
time_us: 1747503000000000
kind: commit
commit:
operation: create
collection: com.publicdomainrelay.temp.market.rfp
rkey: 3mm3doliee72s
cid: bafyreib5u2krsumyya5eiqc7ys7iz3xxlourd34p7qlpehi7a7h2kdc3ia
record:
$type: com.publicdomainrelay.temp.market.rfp
payload:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.compute.vm/3mm3dolfolz2c
cid: bafyreif4toqzci4nu3thujm2quurs4h432qk3gxvmkwze2wrrznn757omi
```
### 4. Bob publishes the x402 pricing payload
```yaml
$type: com.publicdomainrelay.temp.market.bids.x402
cost: 0.10
currency: USDC
frequency: hourly
prepay: true
# {at}/{cid} get replaced by Alice with the accept's AT URI / CID
url: https://compute-contract.bob.example/receipt
# uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bids.x402/3mm4...
```
### 5. Bob publishes the WIF config
`accept_path` is required — Bob will read the fully-resolved accept
bundle from this path inside the VM (cloud-init `write_files` puts it
there). Use `$HOME` in the record; at provision time it resolves to
`/root` for the root-user cloud-init.
```yaml
$type: com.publicdomainrelay.temp.compute.config.wif.simple
accept_path: $HOME/secrets/publicdomainrelay.com/market/accept.json
issuer_uri: https://droplet-oidc.its1337.com
to_issue: api://DigitalOcean?actx=...
token_path: /var/run/secrets/wid/token
url_path: /var/run/secrets/wid/url
url_route: /v1/oidc/issue
subject: actx:<team-uuid>:plc:<alice-did>:role:my-cool-role
# uri: at://did:plc:bob/com.publicdomainrelay.temp.compute.config.wif.simple/3mm4...
```
### 6. Bob publishes the bid envelope
```yaml
$type: com.publicdomainrelay.temp.market.bid
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyreib5u2krsumyya5eiqc7ys7iz3xxlourd34p7qlpehi7a7h2kdc3ia
payload:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bids.x402/3mm4...
cid: bafyrei...x402
config:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.compute.config.wif.simple/3mm4...
cid: bafyrei...wif
```
### 7. Alice runs policy + scoring during the bid window
```mermaid
flowchart LR
A[bids collected<br/>from jetstream] --> P{policy<br/>allow/deny by DID}
P -->|drop| X1[(log reason)]
P --> R[resolve bid.payload]
R --> H{plugin exists<br/>for payload $type?}
H -->|no| X2[(drop + log)]
H -->|yes| S[score: lowest cost wins]
S --> ACC[create market.accept]
```
Defaults (in the reference acceptor): empty allowlist + empty denylist
means "accept all"; scorer is lowest numeric `cost`.
### 8. Alice publishes the Accept
```yaml
$type: com.publicdomainrelay.temp.market.accept
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyreib5u2krsumyya5eiqc7ys7iz3xxlourd34p7qlpehi7a7h2kdc3ia
bid:
$type: com.atproto.repo.strongRef
uri: at://did:plc:bob/com.publicdomainrelay.temp.market.bid/3mm4...
cid: bafyrei...bid
# uri: at://did:plc:alice/com.publicdomainrelay.temp.market.accept/3mlagijgoeb23
# cid: bafyreiamisq3yqgb4k3tdojmzvvzpuwj46ytwbj672zxhyxxl7t36qadz4
```
### 9. Alice triggers payment / settlement
The provider's `bids.x402.url` is concatenated with the accept's URI
and CID and called (GET to probe, or `npx awal x402 pay <url>` to
actually settle):
```
${bids.x402.url}/${accept.uri}/${accept.cid}
=> https://compute-contract.bob.example/receipt/
at://did:plc:alice/com.publicdomainrelay.temp.market.accept/3mlagijgoeb23/
bafyreiamisq3yqgb4k3tdojmzvvzpuwj46ytwbj672zxhyxxl7t36qadz4
```
### 10. Provider relay resolves the chain and provisions
```mermaid
flowchart TD
URL[receive /receipt/<accept.uri>/<accept.cid>] --> ACC[fetch market.accept]
ACC --> BID[fetch market.bid]
BID --> RFP[fetch market.rfp]
RFP --> VM[fetch compute.vm]
BID --> PAY[fetch bids.x402]
BID --> CFG[fetch config.wif.simple]
VM --> BUNDLE[assemble accept bundle JSON]
PAY --> BUNDLE
CFG --> BUNDLE
ACC --> BUNDLE
RFP --> BUNDLE
BUNDLE --> CI[inject write_files + runcmd<br/>into vm.user_data]
CI --> DROPLET[create_droplet]
DROPLET --> RECEIPT[create market.receipt]
```
### 11. Cloud-init bundle dropped on the VM
The provider takes `vm.user_data`, parses it as `#cloud-config`
(creating one if absent), and inserts:
```yaml
#cloud-config
write_files:
- path: /root/secrets/publicdomainrelay.com/market/accept.json
owner: root:root
permissions: '0600'
content: |
{
"accept": { "uri": "...", "cid": "...", "value": { ... } },
"rfp": { "uri": "...", "cid": "...", "value": { ... } },
"bid": { "uri": "...", "cid": "...", "value": { ... } },
"vm": { "uri": "...", "cid": "...", "value": { ... } },
"x402": { "uri": "...", "cid": "...", "value": { ... } },
"wif": { "uri": "...", "cid": "...", "value": { ... } }
}
runcmd:
- [sh, -c, "install -d -m 0700 -o root -g root /root/secrets/publicdomainrelay.com/market"]
# ... whatever else was already in user_data
```
`config.wif.simple.accept_path` (with `$HOME` -> `/root`) tells the
workload inside the VM exactly where to read this file.
### 12. Bob publishes the Receipt
```yaml
$type: com.publicdomainrelay.temp.market.receipt
rfp:
$type: com.atproto.repo.strongRef
uri: at://did:plc:alice/com.publicdomainrelay.temp.market.rfp/3mm3doliee72s
cid: bafyreib5u2krsumyya5eiqc7ys7iz3xxlourd34p7qlpehi7a7h2kdc3ia
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: bafyreiamisq3yqgb4k3tdojmzvvzpuwj46ytwbj672zxhyxxl7t36qadz4
# uri: at://did:plc:bob/com.publicdomainrelay.temp.market.receipt/3mld67yj3xo2u
# cid: bafyreibzynxkkoxxvppbfoeh5s2s2asrm2j7ziw2ol5ufau4q25d7ousiy
```
## Authority and validation rules
```mermaid
flowchart TB
subgraph Alice
RFP1[market.rfp]:::alice
VM1[compute.vm]:::alice
ACC1[market.accept]:::alice
end
subgraph Bob
BID1[market.bid]:::bob
X4[bids.x402]:::bob
WIF[config.wif.simple]:::bob
RCP[market.receipt]:::bob
end
RFP1 --> VM1
BID1 --> RFP1
BID1 --> X4
BID1 --> WIF
ACC1 --> RFP1
ACC1 --> BID1
RCP --> RFP1
RCP --> BID1
RCP --> ACC1
classDef alice fill:#dfe,stroke:#393
classDef bob fill:#def,stroke:#339
```
- `Accept.rfp.uri` MUST equal `Bid.rfp.uri` (and CIDs must match) —
the provider relay refuses to settle otherwise.
- `Accept` MUST be authored by the same DID that authored the
referenced RFP. Otherwise anyone could settle anyone else's RFP.
- `Receipt` MUST be authored by the same DID that authored the
referenced Bid.
- `bids.x402.url` is a template; `{at}` and `{cid}` are placeholders
replaced by Alice with the Accept's AT URI/CID before calling.