Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ name: Deploy JupyterLite
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pull-requests: write

# Only one deployment at a time; cancel in-progress if a new push arrives
# Group by branch; cancel in-progress runs for the same branch/PR
concurrency:
group: pages
group: pages-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -67,8 +70,37 @@ jobs:
run: cp _headers _site/_headers

- name: Deploy to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy _site --project-name=zerokrab-boundless-jupyter --commit-dirty=true
command: pages deploy _site --project-name=zerokrab-boundless-jupyter --commit-dirty=true --branch=${{ github.head_ref || github.ref_name }}

- name: Comment preview URL on PR
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
run: |
PREVIEW_URL=$(echo "$DEPLOY_OUTPUT" | grep -oE 'https://[^ ]*\.pages\.dev' | tail -1)
if [ -z "$PREVIEW_URL" ]; then
echo "Could not extract preview URL from deploy output"
exit 0
fi
# Update or create comment (avoids spam on repeated pushes)
COMMENT_TAG="<!-- cf-pages-preview -->"
BODY="${COMMENT_TAG}
🚀 **Preview deployment ready!**

**URL:** ${PREVIEW_URL}
**Branch:** \`${{ github.head_ref }}\`
**Commit:** \`${{ github.event.pull_request.head.sha }}\`"

# Check for existing comment
EXISTING=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- cf-pages-preview -->")) | .id' | head -1)
if [ -n "$EXISTING" ]; then
gh api graphql -f query="mutation { updateIssueComment(input: {id: \"${EXISTING}\", body: $(echo "$BODY" | jq -Rs .)}) { issueComment { id } } }"
else
gh pr comment ${{ github.event.pull_request.number }} --body "$BODY"
fi
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Instructions for AI agents working on this project.

- **README.md** – Domain background, costs, revenue (POVW + market). Read first for context.
- **boundless_profitability.ipynb** – Single Jupyter notebook: config → cost → revenue → profit → break-even → scenario tables/charts. All figures are **per epoch**. Inputs live under `## 1. Inputs`:
- **1.1 Model Parameters**: `ZKC_PRICES_USD`, `MARKET_ORDER_UTIL`, `MARKET_REWARD_USD_PER_MHZ`, `FIXED_COST_MONTHLY_USD`, and `gpu_configs` (with `label`, `num_gpus`, `hourly_cost_usd`, `mhz`).
- **1.1 Model Parameters**: `ZKC_PRICES_USD`, `MARKET_ORDER_UTIL` (fixed scalar), `MARKET_REWARD_USD_PER_MHZ` (range of scenarios), `FIXED_COST_MONTHLY_USD`, and `gpu_configs` (with `label`, `num_gpus`, `hourly_cost_usd`, `mhz`).
- **1.2 Constants**: `HOURS_PER_EPOCH`, `SECONDS_PER_EPOCH`, `EPOCHS_PER_MONTH`.
- **1.3 POVW Reward Data**: parsing logic that derives `POVW_ZKC_PER_MHZ_PER_EPOCH` from `epochs.csv`.
- **epochs.csv** – Data export: Total Cycles (all provers) and Mining Rewards (ZKC) per epoch. Used to compute **ZKC per mhz per epoch** for POVW. Updated as new data becomes available. The notebook excludes the **latest epoch** (row 1) as it may still be ongoing.
Expand All @@ -26,7 +26,7 @@ Instructions for AI agents working on this project.
| Term | Meaning |
|------|--------|
| POVW rewards | ZKC distributed every epoch based on cycles proven; modeled as ZKC per mhz per epoch. |
| Market rewards | Per-proof payouts; modeled as average USD per mhz, scaled by **market order utilization** (share of capacity fulfilling market orders; e.g. 50%, 75%, 100%). POVW is unchanged by utilization. |
| Market rewards | Per-proof payouts; modeled as a range of USD per mhz scenarios, scaled by a fixed **market order utilization** (share of capacity fulfilling market orders). POVW is unchanged by utilization. |
| ZKC | Cryptocurrency used in Boundless; price in USD is a key input. |
| GPU config | A labeled GPU setup (e.g. `Mock 5090 0.2 5090 x8`) with `label`, `num_gpus`, `mhz` (million cycles/sec), and `hourly_cost_usd`. |

Expand All @@ -36,7 +36,8 @@ Instructions for AI agents working on this project.
- **POVW from data:** `POVW_ZKC_PER_MHZ_PER_EPOCH` is computed from **epochs.csv** (Total Cycles and Mining Rewards); the latest epoch is always excluded. Do not replace this with a literal placeholder.
- **Placeholders:** Other inputs may use placeholders (e.g. `MARKET_REWARD_USD_PER_MHZ`, sample GPU rows). Preserve the structure; replace values with real data when available. Label placeholders in comments or markdown.
- **Cost conversion:** GPU rental is stored **per hour** (`hourly_cost_usd`) and converted to per-epoch as `rental_per_epoch_usd`. Fixed cost is monthly, converted to per-epoch with `EPOCHS_PER_MONTH`, and added **separately** when computing total cost (profit / break-even).
- **Market order utilization:** Configurable (e.g. 50%, 75%, 100%); market revenue = mhz × reward_per_mhz × util. POVW revenue is not scaled by utilization.
- **Market order utilization:** Fixed scalar (e.g. 0.5); market revenue = mhz × reward_per_mhz × util. POVW revenue is not scaled by utilization.
- **Market reward rate:** Configurable range (e.g. $0.00003–$0.0001/MHz); each value produces a separate scenario.

## What to change vs preserve

Expand Down
51 changes: 16 additions & 35 deletions boundless_profitability.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"To estimate profitability we use several inputs:\n",
"- GPU configurations - to adjust costs and MHz capacity\n",
"- ZKC prices - to model for different price points\n",
"- Market order utilization - to adjust for the % of GPU capacity that is spent on market orders, which determines market rewards\n",
"- Market reward rates - to model different expected USD per MHz from market orders\n",
"- Market reward rate - average USD per MHz expected from the market\n",
"\n",
"All costs, revenue, and profit are expressed per epoch and primarily denominated in USD.\n",
Expand Down Expand Up @@ -59,27 +59,7 @@
"id": "fd5c0500b882c0",
"metadata": {},
"source": [
"### 1.1 Model Parameters (EDIT THESE)\n",
"\n",
"`ZKC_PRICES_USD`\n",
"\n",
"The model will use each value as a possible scenario for the price of ZKC. Pick any values you want.\n",
"\n",
"`MARKET_ORDER_UTIL`\n",
"\n",
"A percentage of the GPU capacity the is used for market orders. Pick any values you want. (Note: utilization tends to be <50%)\n",
"\n",
"`MARKET_REWARD_USD_PER_MHZ`\n",
"\n",
"Expected price paid per million cycles on the market. Can be estimated from [market stats](https://explorer.boundless.network/stats), (see \"Lock price per cycle distribution\"). You can divide USD/Bcycle by 1000 to get USD/Mhz.\n",
"\n",
"`FIXED_COST_MONTHLY_USD`\n",
"\n",
"Any additional expenses, fixed cost per month.\n",
"\n",
"`gpu_configs`\n",
"\n",
"List of GPU options to model."
"### 1.1 Model Parameters (EDIT THESE)\n\n`ZKC_PRICES_USD`\n\nThe model will use each value as a possible scenario for the price of ZKC. Pick any values you want.\n\n`MARKET_ORDER_UTIL`\n\nA fixed percentage of GPU capacity used for market orders. (Note: utilization tends to be <50%)\n\n`MARKET_REWARD_USD_PER_MHZ`\n\nA range of expected prices paid per million cycles on the market. The model will use each value as a scenario. Can be estimated from [market stats](https://explorer.boundless.network/stats), (see \"Lock price per cycle distribution\"). You can divide USD/Bcycle by 1000 to get USD/Mhz.\n\n`FIXED_COST_MONTHLY_USD`\n\nAny additional expenses, fixed cost per month.\n\n`gpu_configs`\n\nList of GPU options to model."
]
},
{
Expand All @@ -94,11 +74,11 @@
"# ZKC prices (USD per ZKC) to model\n",
"ZKC_PRICES_USD = [0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.3, 0.5, 1.0]\n",
"\n",
"# Market order utilization: percentage of capacity used for fulfilling market orders.\n",
"MARKET_ORDER_UTIL = [0.25, 0.5, 0.75, 1.0]\n",
"# Market order utilization: fixed percentage of capacity used for market orders\n",
"MARKET_ORDER_UTIL = 0.5\n",
"\n",
"# Average reward per million cycles in USD\n",
"MARKET_REWARD_USD_PER_MHZ = 0.00007\n",
"# Average reward per million cycles in USD (range of scenarios)\n",
"MARKET_REWARD_USD_PER_MHZ = [0.00003, 0.00005, 0.00007, 0.0001]\n",
"\n",
"# Fixed cost additional expenses\n",
"FIXED_COST_MONTHLY_USD = 0\n",
Expand Down Expand Up @@ -253,16 +233,17 @@
" scenario = row[\"label\"]\n",
" for zkc_price in ZKC_PRICES_USD:\n",
" povw_revenue = mhz_per_epoch * POVW_ZKC_PER_MHZ_PER_EPOCH * zkc_price\n",
" for market_order_util in MARKET_ORDER_UTIL:\n",
" market_revenue = mhz_per_epoch * MARKET_REWARD_USD_PER_MHZ * market_order_util\n",
" for market_reward in MARKET_REWARD_USD_PER_MHZ:\n",
" market_revenue = mhz_per_epoch * market_reward * MARKET_ORDER_UTIL\n",
" total_revenue = povw_revenue + market_revenue\n",
" profit = total_revenue - total_cost_epoch\n",
" results.append({\n",
" \"scenario\": scenario,\n",
" \"label\": row[\"label\"],\n",
" \"num_gpus\": row[\"num_gpus\"],\n",
" \"zkc_price_usd\": zkc_price,\n",
" \"market_order_util\": market_order_util,\n",
" \"market_order_util\": MARKET_ORDER_UTIL,\n",
" \"market_reward_usd_per_mhz\": market_reward,\n",
" \"mhz_per_epoch\": mhz_per_epoch,\n",
" \"cost_per_epoch\": total_cost_epoch,\n",
" \"povw_revenue\": povw_revenue,\n",
Expand Down Expand Up @@ -295,7 +276,7 @@
"metadata": {},
"source": [
"# Display summary of data\n",
"revenue_df[[\"scenario\", \"zkc_price_usd\", \"market_order_util\", \"profit_per_epoch\"]]"
"revenue_df[[\"scenario\", \"zkc_price_usd\", \"market_reward_usd_per_mhz\", \"profit_per_epoch\"]]"
],
"outputs": [],
"execution_count": null
Expand All @@ -322,7 +303,7 @@
"metadata": {},
"source": [
"profit_pivot = revenue_df.pivot_table(\n",
" index=[\"scenario\", \"market_order_util\"], columns=\"zkc_price_usd\", values=\"profit_per_epoch\"\n",
" index=[\"scenario\", \"market_reward_usd_per_mhz\"], columns=\"zkc_price_usd\", values=\"profit_per_epoch\"\n",
")\n",
"profit_pivot"
],
Expand All @@ -345,12 +326,12 @@
"%matplotlib inline\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# One graph per GPU config; each shows profit vs ZKC price with one line per market order utilization\n",
"# One graph per GPU config; each shows profit vs ZKC price with one line per market reward rate\n",
"for scenario in revenue_df[\"scenario\"].unique():\n",
" fig, ax = plt.subplots(figsize=(10, 4))\n",
" scenario_df = revenue_df[revenue_df[\"scenario\"] == scenario]\n",
" for util, sub in scenario_df.groupby(\"market_order_util\"):\n",
" ax.plot(sub[\"zkc_price_usd\"], sub[\"profit_per_epoch\"], marker=\"o\", label=f\"{util * 100:.0f}% util\")\n",
" for reward, sub in scenario_df.groupby(\"market_reward_usd_per_mhz\"):\n",
" ax.plot(sub[\"zkc_price_usd\"], sub[\"profit_per_epoch\"], marker=\"o\", label=f\"${reward:.5f}/MHz\")\n",
" ax.axhline(0, color=\"gray\", linestyle=\"--\")\n",
" ax.set_xlabel(\"ZKC price (USD)\")\n",
" ax.set_ylabel(\"Profit per epoch (USD)\")\n",
Expand Down Expand Up @@ -384,7 +365,7 @@
" print(\"No profitable scenarios for current inputs (profit_per_epoch <= 0 everywhere).\")\n",
"else:\n",
" profitable_pivot = profitable_revenue_df.pivot_table(\n",
" index=[\"scenario\", \"market_order_util\"],\n",
" index=[\"scenario\", \"market_reward_usd_per_mhz\"],\n",
" columns=\"zkc_price_usd\",\n",
" values=\"profit_per_epoch\",\n",
" )\n",
Expand Down
36 changes: 18 additions & 18 deletions dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,35 @@ def build_dashboard(df: pd.DataFrame | None = None) -> pn.viewable.Viewable:
df : pd.DataFrame, optional
Pre-computed revenue_df from the notebook. If None, loads results.csv.

Expected columns: scenario, zkc_price_usd, market_order_util,
Expected columns: scenario, zkc_price_usd, market_reward_usd_per_mhz,
profit_per_epoch, povw_revenue, market_revenue,
cost_per_epoch, label
"""
if df is None:
df = pd.read_csv("results.csv")

# Derive available discrete ZKC values from data; use any reference util for interpolation
# Derive available discrete ZKC values from data; use any reference reward for interpolation
zkc_prices = sorted(df["zkc_price_usd"].unique())
ref_util = df["market_order_util"].iloc[0] # reference utilization for rate derivation
ref_reward = df["market_reward_usd_per_mhz"].iloc[0] # reference reward for rate derivation

# ── Widgets ───────────────────────────────────────────────────────────────

zkc_slider = pn.widgets.DiscreteSlider(
name="ZKC Price (USD)", options=zkc_prices, value=zkc_prices[len(zkc_prices) // 2]
)
util_slider = pn.widgets.FloatSlider(
name="Market Utilization", start=0.0, end=1.0, step=0.01, value=0.5,
format="0%",
reward_slider = pn.widgets.FloatSlider(
name="Market Reward (USD/MHz)", start=0.00001, end=0.0002, step=0.00001, value=0.00007,
format="0.00000",
)

# ── Tab 1: Profit Explorer ────────────────────────────────────────────────

@pn.depends(zkc_slider, util_slider)
def profit_chart(zkc_price, market_util):
# Base rows at the reference utilization; scale market_revenue linearly to market_util.
# market_revenue ∝ util (mhz * reward * util), so scaling is exact.
sub = df[(df["zkc_price_usd"] == zkc_price) & (df["market_order_util"] == ref_util)]
scale = market_util / ref_util if ref_util > 0 else 0
@pn.depends(zkc_slider, reward_slider)
def profit_chart(zkc_price, market_reward):
# Base rows at the reference reward; scale market_revenue linearly to market_reward.
# market_revenue ∝ reward (mhz * reward * util), so scaling is exact.
sub = df[(df["zkc_price_usd"] == zkc_price) & (df["market_reward_usd_per_mhz"] == ref_reward)]
scale = market_reward / ref_reward if ref_reward > 0 else 0

labels = sub["label"].tolist()
costs = sub["cost_per_epoch"].tolist()
Expand Down Expand Up @@ -93,11 +93,11 @@ def profit_chart(zkc_price, market_util):

# ── Tab 2: Break-even ─────────────────────────────────────────────────────

@pn.depends(util_slider)
def breakeven_chart(market_util):
# Scale market_revenue to the chosen utilization, then find min profitable ZKC price
scale = market_util / ref_util if ref_util > 0 else 0
sub = df[df["market_order_util"] == ref_util].copy()
@pn.depends(reward_slider)
def breakeven_chart(market_reward):
# Scale market_revenue to the chosen reward, then find min profitable ZKC price
scale = market_reward / ref_reward if ref_reward > 0 else 0
sub = df[df["market_reward_usd_per_mhz"] == ref_reward].copy()
sub = sub.assign(
market_revenue_scaled=sub["market_revenue"] * scale,
)
Expand Down Expand Up @@ -139,7 +139,7 @@ def breakeven_chart(market_util):
sidebar = pn.Column(
"### Controls",
zkc_slider,
util_slider,
reward_slider,
width=240,
)

Expand Down
Loading