From f344de7aee008d1c6d78bf807d47f81a35d7db08 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 19 Mar 2026 21:19:12 +0000 Subject: [PATCH 1/4] swap market utilization (range) for market reward rate (range) - MARKET_ORDER_UTIL: changed from list of scenarios to fixed scalar (0.5) - MARKET_REWARD_USD_PER_MHZ: changed from fixed value to list of scenarios - Updated computation loop, pivot tables, and graphs to iterate over reward rates - Updated dashboard sliders: replaced utilization slider with reward rate slider - Updated AGENTS.md to reflect the new parameter roles --- AGENTS.md | 7 ++--- boundless_profitability.ipynb | 51 +++++++++++------------------------ dashboard.py | 36 ++++++++++++------------- 3 files changed, 38 insertions(+), 56 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bc84727..6c7e252 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. @@ -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`. | @@ -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 diff --git a/boundless_profitability.ipynb b/boundless_profitability.ipynb index 77f8740..4ea8123 100644 --- a/boundless_profitability.ipynb +++ b/boundless_profitability.ipynb @@ -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", @@ -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." ] }, { @@ -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", @@ -253,8 +233,8 @@ " 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", @@ -262,7 +242,8 @@ " \"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", @@ -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 @@ -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" ], @@ -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", @@ -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", diff --git a/dashboard.py b/dashboard.py index 22c7fda..84c5093 100644 --- a/dashboard.py +++ b/dashboard.py @@ -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() @@ -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, ) @@ -139,7 +139,7 @@ def breakeven_chart(market_util): sidebar = pn.Column( "### Controls", zkc_slider, - util_slider, + reward_slider, width=240, ) From 408961a79d5b88ef618c14d16364282f695cdb20 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 19 Mar 2026 21:28:55 +0000 Subject: [PATCH 2/4] add local development instructions to README Documents the full build pipeline: install deps, run notebook, build JupyterLite + Panel dashboard, and serve locally. Extracted from the GitHub Actions workflow. --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1683611..3b0b034 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,70 @@ Visit [jupyter.zerokrab.com](https://jupyter.zerokrab.com) for a live hosted ver ## Usage - **Online** - visit [jupyter.zerokrab.com](https://jupyter.zerokrab.com) -- **Local** - Requires Python 3.x +- **Local** - Requires Python 3.11+ See `How to run` at the top of the notebook. + +## Local Development + +### Prerequisites + +Python 3.11+ is required. + +### Install dependencies + +```bash +pip install \ + jupyterlite-core \ + jupyterlite-pyodide-kernel \ + jupyter-server \ + panel \ + pandas numpy matplotlib nbconvert ipykernel +``` + +### Run the notebook + +```bash +jupyter notebook +``` + +This opens the browser UI where you can run `boundless_profitability.ipynb` interactively. + +### Build the full site (JupyterLite + Panel dashboard) + +This replicates the CI/CD pipeline locally: + +```bash +# 1. Execute the notebook to generate results.csv +jupyter nbconvert --to notebook --execute \ + --ExecutePreprocessor.timeout=120 \ + boundless_profitability.ipynb \ + --output boundless_profitability.ipynb + +# 2. Build JupyterLite static site +jupyter lite build \ + --contents boundless_profitability.ipynb \ + --contents epochs.csv \ + --contents dashboard.py \ + --output-dir _site + +# 3. Build Panel standalone dashboard +panel convert dashboard.py \ + --to pyodide-worker \ + --out _site/dashboard/ +cp results.csv _site/dashboard/results.csv + +# 4. Copy CORS headers (needed for SharedArrayBuffer / Pyodide) +cp _headers _site/_headers +``` + +### Serve the built site locally + +```bash +cd _site +python -m http.server 8000 +``` + +Then open http://localhost:8000 in your browser. + +> **Note:** Some browsers require CORS headers (`Cross-Origin-Opener-Policy` / `Cross-Origin-Embedder-Policy`) for Pyodide to work. The `_headers` file handles this on Cloudflare Pages. For local dev, you may need a server that sets these headers, or use the `jupyter notebook` workflow above instead. From e2cd987db46077da1362645eacf89b50afa39bf7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 19 Mar 2026 21:32:42 +0000 Subject: [PATCH 3/4] Revert "add local development instructions to README" This reverts commit 408961a79d5b88ef618c14d16364282f695cdb20. --- README.md | 66 +------------------------------------------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/README.md b/README.md index 3b0b034..1683611 100644 --- a/README.md +++ b/README.md @@ -11,70 +11,6 @@ Visit [jupyter.zerokrab.com](https://jupyter.zerokrab.com) for a live hosted ver ## Usage - **Online** - visit [jupyter.zerokrab.com](https://jupyter.zerokrab.com) -- **Local** - Requires Python 3.11+ +- **Local** - Requires Python 3.x See `How to run` at the top of the notebook. - -## Local Development - -### Prerequisites - -Python 3.11+ is required. - -### Install dependencies - -```bash -pip install \ - jupyterlite-core \ - jupyterlite-pyodide-kernel \ - jupyter-server \ - panel \ - pandas numpy matplotlib nbconvert ipykernel -``` - -### Run the notebook - -```bash -jupyter notebook -``` - -This opens the browser UI where you can run `boundless_profitability.ipynb` interactively. - -### Build the full site (JupyterLite + Panel dashboard) - -This replicates the CI/CD pipeline locally: - -```bash -# 1. Execute the notebook to generate results.csv -jupyter nbconvert --to notebook --execute \ - --ExecutePreprocessor.timeout=120 \ - boundless_profitability.ipynb \ - --output boundless_profitability.ipynb - -# 2. Build JupyterLite static site -jupyter lite build \ - --contents boundless_profitability.ipynb \ - --contents epochs.csv \ - --contents dashboard.py \ - --output-dir _site - -# 3. Build Panel standalone dashboard -panel convert dashboard.py \ - --to pyodide-worker \ - --out _site/dashboard/ -cp results.csv _site/dashboard/results.csv - -# 4. Copy CORS headers (needed for SharedArrayBuffer / Pyodide) -cp _headers _site/_headers -``` - -### Serve the built site locally - -```bash -cd _site -python -m http.server 8000 -``` - -Then open http://localhost:8000 in your browser. - -> **Note:** Some browsers require CORS headers (`Cross-Origin-Opener-Policy` / `Cross-Origin-Embedder-Policy`) for Pyodide to work. The `_headers` file handles this on Cloudflare Pages. For local dev, you may need a server that sets these headers, or use the `jupyter notebook` workflow above instead. From f93f28af1ffbaac7490a68fb36f5e2cddb0263a1 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 19 Mar 2026 21:33:53 +0000 Subject: [PATCH 4/4] add Cloudflare Pages preview deployments for PRs - Trigger workflow on pull_request events (in addition to push/main) - Pass --branch flag to wrangler to create preview deployments - Post/update preview URL as a PR comment (avoids duplicate comments) - Scope concurrency group per branch to allow parallel PR previews --- .github/workflows/deploy.yml | 38 +++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a23dac5..aebdec2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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: @@ -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="" + 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("")) | .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