Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
34 changes: 34 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,34 @@
"""Add alpha burn payment columns

Revision ID: a1b2c3d4e5f6
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 = "a1b2c3d4e5f6"
down_revision: Union[str, Sequence[str], None] = "e7f3a1b2c905"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


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)


def downgrade() -> None:
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")
140 changes: 94 additions & 46 deletions api/src/endpoints/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@
from api.src.utils.openrouter_validation import validate_openrouter_keys
from api.src.utils.request_cache import hourly_cache
from api.src.utils.upload_agent_helpers import (
SN62_NETUID,
as_utc,
check_agent_banned,
check_file_size,
check_hotkey_registered,
check_if_python_file,
check_rate_limit,
check_signature,
get_alpha_price,
get_miner_hotkey,
get_tao_price,
timestamp_ms_to_utc_datetime,
)
from models.agent import Agent, AgentCreate, AgentStatus
Expand Down Expand Up @@ -50,6 +51,14 @@
UPLOAD_PAYMENT_QUOTE_TTL_SECONDS = 60 * 60
OUTDATED_UPLOAD_CLIENT_MESSAGE = "This upload client is outdated. Please upgrade Ridges CLI and retry."

BURN_CALL_FUNCTIONS = frozenset({"burn_alpha", "add_stake_burn"})
# Candidate attribute keys on the AlphaBurned event, in priority order (exact label, then snake_case).
_ALPHA_BURN_KEYS = {
"coldkey": ("Coldkey", "coldkey"),
"netuid": ("Netuid", "netuid"),
"amount": ("Actual Alpha Decrease", "actual_alpha_decrease", "alpha", "amount"),
}

Comment thread
jmnmv12 marked this conversation as resolved.
Outdated
# We use a lock per hotkey to prevent multiple agents being uploaded at the same time for the same hotkey
hotkey_locks: dict[str, asyncio.Lock] = {}
hotkey_locks_lock = asyncio.Lock()
Expand Down Expand Up @@ -94,12 +103,15 @@ 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
miner_alpha_stake = (await subtensor_client.get_alpha_stake(coldkey)).rao
payment_cost = await get_upload_price()
if payment_cost.amount_rao > miner_balance:
if payment_cost.amount_alpha_rao > miner_alpha_stake:
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"staked on SN{SN62_NETUID} to upload. You have {miner_alpha_stake}."
),
)
await validate_openrouter_keys(
openrouter_api_key=openrouter_api_key,
Expand All @@ -108,16 +120,14 @@ 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,
expires_at=quote.expires_at,
)

Expand Down Expand Up @@ -231,7 +241,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 +251,42 @@ 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")
if await check_if_extrinsic_failed(payment_block_hash, payment_extrinsic_index_int):
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.
events = await subtensor_client.get_events(block_hash=payment_block_hash)
burn_attrs = find_alpha_burned_event(events, payment_extrinsic_index_int)

event_netuid = _event_attr(burn_attrs, *_ALPHA_BURN_KEYS["netuid"])
if event_netuid != SN62_NETUID:
raise HTTPException(status_code=402, detail=f"Burn is not on SN{SN62_NETUID}")

event_coldkey = _event_attr(burn_attrs, *_ALPHA_BURN_KEYS["coldkey"])
if 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}",
)
burned_alpha_rao = _event_attr(burn_attrs, *_ALPHA_BURN_KEYS["amount"])
if burned_alpha_rao is None:
raise HTTPException(status_code=402, detail="Burn amount not found")
burned_alpha_rao = int(burned_alpha_rao)
if burned_alpha_rao < 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 = burned_alpha_rao

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 +314,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,17 +418,18 @@ 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()
eval_cost_usd = 5

# Get the amount of tao required per eval
eval_cost_tao = eval_cost_usd / TAO_PRICE
# Alpha required to cover the eval cost at the current alpha price.
eval_cost_alpha = eval_cost_usd / ALPHA_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)
# Keep the 1.4x buffer. Burned alpha is destroyed (not reclaimable), so the buffer now
# absorbs alpha-price movement between quote and burn and keeps prod uploads meaningfully
# more expensive than local testing to discourage variance farming.
amount_alpha_rao = int(eval_cost_alpha * 1e9 * 1.4)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40% is a lot, maybe 10%?


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


async def check_if_extrinsic_failed(block_hash: str, extrinsic_index: int) -> bool:
Expand All @@ -438,3 +446,43 @@ async def check_if_extrinsic_failed(block_hash: str, extrinsic_index: int) -> bo
return True

return False


def _event_attr(attributes, *names):
"""Read a named attribute from an event's attributes, tolerating dict or [{name,value}] shapes."""
if isinstance(attributes, dict):
for name in names:
if name in attributes:
return attributes[name]
return None
for entry in attributes or []:
if isinstance(entry, dict) and entry.get("name") in names:
return entry.get("value")
return None


def find_alpha_burned_event(events: list, extrinsic_index: int) -> dict:
"""Return the attributes of the SubtensorModule.AlphaBurned event for the given extrinsic index."""
for event in events:
if event.get("extrinsic_idx") != extrinsic_index:
continue
inner = event.get("event", {})
if inner.get("module_id") == "SubtensorModule" and inner.get("event_id") == "AlphaBurned":
return inner.get("attributes", {})
raise HTTPException(status_code=402, detail="Burn event not found")


def verify_burn_extrinsic(extrinsic, expected_coldkey: str) -> None:
"""Cross-check the extrinsic at the burn index: recognized burn call, signed by the expected coldkey."""
try:
value = extrinsic.value_serialized
call = value["call"]
signer = value["address"]
except (KeyError, TypeError, AttributeError):
raise HTTPException(status_code=402, detail="Burn extrinsic could not be decoded") from None

if call.get("call_module") != "SubtensorModule" or call.get("call_function") not in BURN_CALL_FUNCTIONS:
raise HTTPException(status_code=402, detail="Extrinsic is not a recognized alpha burn")

if signer != expected_coldkey:
raise HTTPException(status_code=402, detail="Burn was not signed by the miner coldkey")
10 changes: 10 additions & 0 deletions api/src/utils/upload_agent_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,13 @@ async def get_tao_price() -> float:
data = r.json()

return data["bittensor"]["usd"]


SN62_NETUID = 62

Comment thread
jmnmv12 marked this conversation as resolved.
Outdated

async def get_alpha_price() -> float:
"""Return the SN62 alpha price in USD: (alpha price in TAO from chain) * (TAO price in USD)."""
alpha_tao = await subtensor_client.get_alpha_price_tao()
tao_usd = await get_tao_price()
return alpha_tao * tao_usd
8 changes: 5 additions & 3 deletions db/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class EvaluationPayment(Base, CreatedAtMixin):
)
miner_hotkey: Mapped[str] = mapped_column(sa.Text, nullable=False)
miner_coldkey: Mapped[str] = mapped_column(sa.Text, nullable=False)
amount_rao: Mapped[int] = mapped_column(sa.Integer, nullable=False)
amount_rao: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True)
amount_alpha_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True)

__table_args__ = (sa.PrimaryKeyConstraint("payment_block_hash", "payment_extrinsic_index"),)

Expand All @@ -42,6 +43,7 @@ class UploadPaymentQuote(Base, CreatedAtMixin):
server_default=sa.text("gen_random_uuid()"),
)
miner_hotkey: Mapped[str] = mapped_column(sa.Text, nullable=False)
amount_rao: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
send_address: Mapped[str] = mapped_column(sa.Text, nullable=False)
amount_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True)
amount_alpha_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True)
send_address: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True)
Comment thread
jmnmv12 marked this conversation as resolved.
expires_at: Mapped[datetime] = mapped_column(sa.TIMESTAMP(timezone=True), nullable=False)
19 changes: 10 additions & 9 deletions miners/cli/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@ def _unlock_coldkey(wallet) -> None:


def _confirm_payment(payment_method_details: dict) -> bool:
amount_alpha = payment_method_details["amount_alpha_rao"] / 1e9
confirm_payment = Prompt.ask(
(
f"\n[bold yellow]Proceed with payment of {payment_method_details['amount_rao']} RAO "
f"({payment_method_details['amount_rao'] / 1e9} TAO) to "
f"{payment_method_details['send_address']}?[/bold yellow]"
f"\n[bold yellow]Proceed with an IRREVERSIBLE burn of {amount_alpha:,.4f} alpha "
f"({payment_method_details['amount_alpha_rao']} in 1e9 units) on SN62?[/bold yellow]"
),
choices=["y", "n"],
default="n",
Expand All @@ -211,11 +211,12 @@ def _submit_eval_payment(*, wallet, payment_method_details: dict) -> PaymentRece

subtensor = Subtensor(network=os.environ.get("SUBTENSOR_NETWORK", "finney"))
payment_payload = subtensor.substrate.compose_call(
call_module="Balances",
call_function="transfer_keep_alive",
call_module="SubtensorModule",
call_function="burn_alpha",
call_params={
"dest": payment_method_details["send_address"],
"value": payment_method_details["amount_rao"],
"hotkey": wallet.hotkey.ss58_address,
"amount": payment_method_details["amount_alpha_rao"],
"netuid": 62,
},
)

Expand All @@ -233,8 +234,8 @@ def _submit_eval_payment(*, wallet, payment_method_details: dict) -> PaymentRece

def _print_payment_receipt(receipt: PaymentReceipt) -> None:
console.print(
"\n[yellow]Payment extrinsic submitted. If something goes wrong with the upload, "
"you can use this information to get a refund[/yellow]"
"\n[yellow]Burn extrinsic submitted. Burns are irreversible; if the upload fails, "
"use this info with `ridges resume-upload` to retry (not for a refund)[/yellow]"
)
if receipt.quote_id:
console.print(f"[cyan]Payment Quote ID:[/cyan] {receipt.quote_id}")
Expand Down
8 changes: 5 additions & 3 deletions models/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ class Payment(BaseModel):

miner_hotkey: str
miner_coldkey: str
amount_rao: int
amount_rao: Optional[int] = None
amount_alpha_rao: Optional[int] = None

created_at: datetime


class PaymentQuote(BaseModel):
quote_id: UUID
miner_hotkey: str
amount_rao: int
send_address: str
amount_rao: Optional[int] = None
amount_alpha_rao: Optional[int] = None
send_address: Optional[str] = None
created_at: datetime
expires_at: datetime
8 changes: 3 additions & 5 deletions models/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@ class AgentUploadResponse(BaseModel):
class UploadPriceResponse(BaseModel):
"""Response model for upload pricing"""

amount_rao: int = Field(..., description="Amount to send for evaluation (in RAO)")
send_address: str = Field(..., description="TAO address to send evaluation payment to")
amount_alpha_rao: int = Field(..., description="Amount of SN62 alpha to burn (in 1e9 units)")


class AgentCheckResponse(AgentUploadResponse):
"""Response model for successful agent upload preflight checks"""

quote_id: UUID = Field(..., description="Quote ID to include when uploading or resuming")
amount_rao: int = Field(..., description="Amount to send for evaluation (in RAO)")
send_address: str = Field(..., description="TAO address to send evaluation payment to")
expires_at: datetime = Field(..., description="Latest on-chain payment timestamp accepted for this quote")
amount_alpha_rao: int = Field(..., description="Amount of SN62 alpha to burn (in 1e9 units)")
expires_at: datetime = Field(..., description="Latest on-chain burn timestamp accepted for this quote")


class ErrorResponse(BaseModel):
Expand Down
Loading