Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6bc3ca8
feat: derive alpha price on-chain via get_subnet_price
jmnmv12 Jul 8, 2026
1fbafd3
fix: restore SN62_NETUID constant and get_alpha_price_tao signature
jmnmv12 Jul 8, 2026
cd56bfa
fix: use config.NETUID in get_alpha_price_tao, matching SubtensorClie…
jmnmv12 Jul 8, 2026
123609c
feat: add get_alpha_stake to SubtensorClient
jmnmv12 Jul 8, 2026
69135b4
feat: add amount_alpha_rao columns for alpha-burn payments
jmnmv12 Jul 8, 2026
16cc259
feat: return amount_alpha_rao from upload pricing responses
jmnmv12 Jul 8, 2026
bdad03f
feat: store amount_alpha_rao in payment quotes and reservations
jmnmv12 Jul 8, 2026
80e95f1
feat: add alpha-burn event and extrinsic verification helpers
jmnmv12 Jul 8, 2026
a45e101
feat: verify alpha burn via AlphaBurned event in upload endpoint
jmnmv12 Jul 8, 2026
635f9fe
feat: CLI submits SN62 alpha burn instead of TAO transfer
jmnmv12 Jul 8, 2026
2dab867
polish: minor fixes
jmnmv12 Jul 8, 2026
f331cd7
fix: :bug: Address PR comments
jmnmv12 Jul 9, 2026
07b5794
refactor: :recycle: Refactor test structure and alphaburned event exa…
jmnmv12 Jul 9, 2026
14f443f
fix: :card_file_box: Add constraint to validate that at least one of …
jmnmv12 Jul 9, 2026
f07b7d1
fix: :bug: Address PR comments
jmnmv12 Jul 10, 2026
3331147
test: :white_check_mark: Fix failing tests
jmnmv12 Jul 10, 2026
89d698e
avoid migration failing
ibraheem-abe Jul 10, 2026
d14dcd0
update bittensor deps
ibraheem-abe Jul 10, 2026
baed6c9
add get_alpha_stake_availability: check stake on CK + HK and lock as …
ibraheem-abe Jul 10, 2026
51460c5
update load to use get_alpha_stake_availability
ibraheem-abe Jul 10, 2026
b371898
add payment netuid + alpha price netuid
ibraheem-abe Jul 10, 2026
d39560f
tests
ibraheem-abe Jul 10, 2026
1da7a6d
fix bad revision id
ibraheem-abe Jul 13, 2026
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
58 changes: 58 additions & 0 deletions alembic/versions/2026_07_07_alpha_burn_payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Add alpha burn payment columns

Revision ID: c8f41e9a2b37
Revises: e7f3a1b2c905
Create Date: 2026-07-07 00:00:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

revision: str = "c8f41e9a2b37"
down_revision: Union[str, Sequence[str], None] = "e7f3a1b2c905"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


_CHECK_NAME = "ck_amount_rao_xor_amount_alpha_rao"
_CHECK_SQL = "num_nonnulls(amount_rao, amount_alpha_rao) = 1"


def upgrade() -> None:
op.add_column("evaluation_payments", sa.Column("amount_alpha_rao", sa.BigInteger(), nullable=True))
op.add_column("upload_payment_quotes", sa.Column("amount_alpha_rao", sa.BigInteger(), nullable=True))
op.alter_column("evaluation_payments", "amount_rao", existing_type=sa.Integer(), nullable=True)
op.alter_column("upload_payment_quotes", "amount_rao", existing_type=sa.BigInteger(), nullable=True)
op.alter_column("upload_payment_quotes", "send_address", existing_type=sa.Text(), nullable=True)
op.create_check_constraint(_CHECK_NAME, "evaluation_payments", _CHECK_SQL)
op.create_check_constraint(_CHECK_NAME, "upload_payment_quotes", _CHECK_SQL)


def downgrade() -> None:
alpha_row_count = op.get_bind().scalar(
sa.text(
"""
SELECT
(SELECT count(*) FROM upload_payment_quotes WHERE amount_alpha_rao IS NOT NULL)
+
(SELECT count(*) FROM evaluation_payments WHERE amount_alpha_rao IS NOT NULL)
"""
)
)
if alpha_row_count:
raise RuntimeError(
"Cannot downgrade alpha burn payments while alpha-denominated quotes or payments exist; "
"the legacy schema cannot represent those rows."
)

op.drop_constraint(_CHECK_NAME, "upload_payment_quotes", type_="check")
op.drop_constraint(_CHECK_NAME, "evaluation_payments", type_="check")
op.alter_column("upload_payment_quotes", "send_address", existing_type=sa.Text(), nullable=False)
op.alter_column("upload_payment_quotes", "amount_rao", existing_type=sa.BigInteger(), nullable=False)
op.alter_column("evaluation_payments", "amount_rao", existing_type=sa.Integer(), nullable=False)
op.drop_column("upload_payment_quotes", "amount_alpha_rao")
op.drop_column("evaluation_payments", "amount_alpha_rao")
5 changes: 1 addition & 4 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@


# Load Bittensor configuration
NETUID = os.getenv("NETUID")
if not NETUID:
logger.fatal("NETUID is not set in .env")
NETUID = int(NETUID)
NETUID = int(os.getenv("NETUID") or "62")

SUBTENSOR_ADDRESS = os.getenv("SUBTENSOR_ADDRESS")
if not SUBTENSOR_ADDRESS:
Expand Down
126 changes: 64 additions & 62 deletions api/src/endpoints/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
check_agent_banned,
check_file_size,
check_hotkey_registered,
check_if_extrinsic_failed,
check_if_python_file,
check_rate_limit,
check_signature,
find_alpha_burned_event,
get_alpha_price,
get_miner_hotkey,
get_tao_price,
timestamp_ms_to_utc_datetime,
verify_burn_extrinsic,
)
from models.agent import Agent, AgentCreate, AgentStatus
from models.upload import AgentCheckResponse, AgentUploadResponse, ErrorResponse, UploadPriceResponse
Expand Down Expand Up @@ -94,12 +97,29 @@ async def check_agent_post(
check_if_python_file(agent_file.filename)
await check_file_size(agent_file)
coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey)
miner_balance = (await subtensor_client.get_balance(coldkey)).rao
if coldkey is None:
raise HTTPException(status_code=400, detail="Hotkey owner not found")

try:
alpha_stake = await subtensor_client.get_alpha_stake_availability(
coldkey=coldkey,
hotkey=miner_hotkey,
netuid=config.NETUID,
)
except Exception as e:
logger.error(f"Error retrieving burnable alpha stake: {e}")
raise HTTPException(status_code=503, detail="Burnable alpha stake could not be verified") from e

payment_cost = await get_upload_price()
if payment_cost.amount_rao > miner_balance:
if payment_cost.amount_alpha_rao > alpha_stake.burnable_rao:
raise HTTPException(
status_code=402,
detail=f"Insufficient balance. You need {payment_cost.amount_rao} RAO to upload this agent. You have {miner_balance} RAO.",
detail=(
f"Insufficient alpha. You need {payment_cost.amount_alpha_rao} alpha (1e9 units) "
f"burnable from the miner hotkey position on SN{config.NETUID}. "
f"Position: {alpha_stake.position_rao}; subnet total: {alpha_stake.total_rao}; "
f"locked: {alpha_stake.locked_rao}; burnable: {alpha_stake.burnable_rao}."
),
)
await validate_openrouter_keys(
openrouter_api_key=openrouter_api_key,
Expand All @@ -108,16 +128,15 @@ async def check_agent_post(
expires_at = datetime.now(timezone.utc) + timedelta(seconds=UPLOAD_PAYMENT_QUOTE_TTL_SECONDS)
quote = await create_payment_quote(
miner_hotkey=miner_hotkey,
amount_rao=payment_cost.amount_rao,
send_address=payment_cost.send_address,
amount_alpha_rao=payment_cost.amount_alpha_rao,
expires_at=expires_at,
)
return AgentCheckResponse(
status="success",
message="Agent check successful",
quote_id=quote.quote_id,
amount_rao=quote.amount_rao,
send_address=quote.send_address,
amount_alpha_rao=quote.amount_alpha_rao,
payment_netuid=config.NETUID,
expires_at=quote.expires_at,
)

Expand Down Expand Up @@ -217,6 +236,9 @@ async def post_agent(
if quote.miner_hotkey != miner_hotkey:
raise HTTPException(status_code=402, detail="Payment quote does not match upload hotkey")

if quote.amount_alpha_rao is None:
raise HTTPException(status_code=400, detail=OUTDATED_UPLOAD_CLIENT_MESSAGE)

existing_payment = await retrieve_payment_by_hash(
payment_block_hash=payment_block_hash, payment_extrinsic_index=payment_extrinsic_index
)
Expand All @@ -231,7 +253,7 @@ async def post_agent(
logger.warning(f"Payment with block hash {payment_block_hash} has been refunded. Rejecting upload.")
raise PaymentRefunded()

# Retrieve payment details from the chain
# Retrieve the burn block + events from the chain
try:
payment_block_info = await subtensor_client.get_block_info(block_hash=payment_block_hash)
except Exception as e:
Expand All @@ -241,45 +263,40 @@ async def post_agent(
if payment_block_info is None:
raise HTTPException(status_code=402, detail="Payment block not found")

coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey, block=int(payment_block_info.number))
try:
payment_extrinsic_index_int = int(payment_extrinsic_index)
if payment_extrinsic_index_int < 0:
raise ValueError
payment_extrinsic = payment_block_info.extrinsics[payment_extrinsic_index_int]
payment_extrinsic_value = payment_extrinsic.value_serialized
payment_call = payment_extrinsic_value["call"]
call_args = {arg["name"]: arg["value"] for arg in payment_call["call_args"]}
payment_value = call_args.get("value")
destination = call_args.get("dest")
payment_address = payment_extrinsic_value["address"]
except (ValueError, TypeError, IndexError, KeyError, AttributeError):
raise HTTPException(status_code=402, detail="Payment extrinsic could not be decoded") from None

if (
payment_call.get("call_module") != "Balances"
or payment_call.get("call_function") != "transfer_keep_alive"
):
raise HTTPException(status_code=402, detail="Payment extrinsic is not a TAO transfer")
except (ValueError, TypeError, IndexError, AttributeError):
raise HTTPException(status_code=402, detail="Burn extrinsic could not be decoded") from None

if payment_value is None or await check_if_extrinsic_failed(
payment_block_hash, payment_extrinsic_index_int
):
raise HTTPException(status_code=402, detail="Payment value not found")
events = await subtensor_client.get_events(block_hash=payment_block_hash)
if await check_if_extrinsic_failed(payment_extrinsic_index_int, events):
raise HTTPException(status_code=402, detail="Burn extrinsic failed on-chain")

coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey, block=int(payment_block_info.number))

# Make sure coldkey is the same as hotkeys owner coldkey
if coldkey != payment_address:
# Cross-check the extrinsic: recognized burn call signed by the miner coldkey.
verify_burn_extrinsic(payment_extrinsic, expected_coldkey=coldkey)

# Event is the source of truth for amount, netuid, and burner.
burn_event = find_alpha_burned_event(
events,
payment_extrinsic_index_int,
netuid=config.NETUID,
)

if burn_event.coldkey != coldkey:
raise HTTPException(status_code=402, detail="Coldkey does not match")

# Make sure destination is our upload send address
if destination != quote.send_address:
raise HTTPException(
status_code=402,
detail=f"Destination does not match. The payment should be sent to {quote.send_address}",
)
if burn_event.hotkey != miner_hotkey:
raise HTTPException(status_code=402, detail="Hotkey does not match")

if burn_event.alpha_decrease < quote.amount_alpha_rao:
raise HTTPException(status_code=402, detail="Burn amount too low")

if payment_value != quote.amount_rao:
raise HTTPException(status_code=402, detail="Payment amount does not match")
payment_value = burn_event.alpha_decrease

payment_block_time = timestamp_ms_to_utc_datetime(payment_block_info.timestamp)
if not (as_utc(quote.created_at) <= payment_block_time <= as_utc(quote.expires_at)):
Expand Down Expand Up @@ -307,7 +324,7 @@ async def post_agent(
payment_extrinsic_index=payment_extrinsic_index,
miner_hotkey=miner_hotkey,
miner_coldkey=coldkey,
amount_rao=payment_value,
amount_alpha_rao=payment_value,
quote_id=quote.quote_id,
)

Expand Down Expand Up @@ -411,30 +428,15 @@ async def post_agent(
@router.get("/eval-pricing", tags=["eval-pricing"], response_model=UploadPriceResponse)
@hourly_cache()
async def get_upload_price() -> UploadPriceResponse:
TAO_PRICE = await get_tao_price()
ALPHA_PRICE = await get_alpha_price(config.NETUID)
eval_cost_usd = 5

# Get the amount of tao required per eval
eval_cost_tao = eval_cost_usd / TAO_PRICE

# Add a buffer against price fluctuations and eval cost variance. If this is over, we burn the difference. Determined EoD by net eval charges - net amount received
# This also makes production evals more expensive than local by a good margin to discourage testing in production and variance farming
amount_rao = int(eval_cost_tao * 1e9 * 1.4)

return UploadPriceResponse(amount_rao=amount_rao, send_address=config.UPLOAD_SEND_ADDRESS)


async def check_if_extrinsic_failed(block_hash: str, extrinsic_index: int) -> bool:
events = await subtensor_client.get_events(block_hash=block_hash)

for event in events:
if event.get("extrinsic_idx") != extrinsic_index:
continue

module = event["event"]["module_id"]
event_id = event["event"]["event_id"]
# Alpha required to cover the eval cost at the current alpha price.
eval_cost_alpha = eval_cost_usd / ALPHA_PRICE

if module == "System" and event_id == "ExtrinsicFailed":
return True
# 1.1x buffer. Burned alpha is destroyed (not reclaimable), so the buffer absorbs
# alpha-price movement between quote and burn while keeping prod uploads a bit more
# expensive than local testing to discourage variance farming.
amount_alpha_rao = int(eval_cost_alpha * 1e9 * 1.1)

return False
return UploadPriceResponse(amount_alpha_rao=amount_alpha_rao, payment_netuid=config.NETUID)
Loading
Loading