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
6 changes: 4 additions & 2 deletions alembic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ production database match the Grid-owned schema contracts without relying on

- `env.py` - Alembic environment.
- `script.py.mako` - revision template.
- `versions/` - ordered migration revisions. Current head: `0016`
- `versions/` - ordered migration revisions. Current head: `0018`
(`0009` payout-pref cols, `0010` grid_revenue, `0011` grid_payout_legs,
`0012` reservations.free_micro, `0013` universal identities, scoped keys,
promotional grants, and reservations.promo_micro; `0014` codifies safe DB
defaults for Grid-native inserts into the optional legacy waiting-prompts
table; `0015` adds native service clients, expiring-key metadata, and
reservation-time price snapshots; `0016` reconciles early production
constraint drift for ledger idempotency and validator evidence).
constraint drift for ledger idempotency and validator evidence; `0017` adds
immutable Base deposit receipts committed with purchased-credit movements;
`0018` records x402 reservation provenance and on-chain settlement receipts).

## Local Contracts

Expand Down
9 changes: 8 additions & 1 deletion alembic/versions/0003_credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ def upgrade() -> None:
)
op.create_table(
"grid_credit_ledger",
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
# SQLite only autoincrements an exact INTEGER PRIMARY KEY. Keep the
# canonical Postgres BIGINT while matching v2 schema on embedded grids.
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
primary_key=True,
autoincrement=True,
),
sa.Column("account_id", sa.Uuid, sa.ForeignKey("grid_accounts.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("delta_micro", sa.BigInteger, nullable=False),
sa.Column("reason", sa.String(64), nullable=False),
Expand Down
72 changes: 72 additions & 0 deletions alembic/versions/0017_funding_deposits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-FileCopyrightText: 2026 AI Power Grid
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Add immutable Base funding receipts.

Revision ID: 0017
Revises: 0016
Create Date: 2026-07-27
"""

import sqlalchemy as sa

from alembic import op

revision = "0017"
down_revision = "0016"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"grid_deposits",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
primary_key=True,
autoincrement=True,
),
sa.Column(
"account_id",
sa.Uuid(),
sa.ForeignKey("grid_accounts.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("chain_id", sa.BigInteger(), nullable=False),
sa.Column("asset", sa.String(length=12), nullable=False),
sa.Column("token_address", sa.String(length=42), nullable=True),
sa.Column("tx_hash", sa.String(length=66), nullable=False),
sa.Column("block_number", sa.BigInteger(), nullable=False),
sa.Column("from_address", sa.String(length=42), nullable=False),
sa.Column("treasury_address", sa.String(length=42), nullable=False),
sa.Column("amount_raw", sa.Numeric(precision=78, scale=0), nullable=False),
sa.Column("amount_decimals", sa.Integer(), nullable=False),
sa.Column("price_micro", sa.BigInteger(), nullable=False),
sa.Column("price_source", sa.String(length=128), nullable=False),
sa.Column("price_timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("price_block", sa.BigInteger(), nullable=True),
sa.Column("credited_micro", sa.BigInteger(), nullable=False),
sa.Column("refund_address", sa.String(length=42), nullable=False),
sa.Column("status", sa.String(length=24), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), nullable=False),
sa.CheckConstraint("amount_raw > 0", name="ck_grid_deposit_positive_amount"),
sa.CheckConstraint("credited_micro > 0", name="ck_grid_deposit_positive_credit"),
sa.UniqueConstraint(
"chain_id",
"asset",
"tx_hash",
name="uq_grid_deposit_chain_asset_tx",
),
)
op.create_index("ix_grid_deposits_account_id", "grid_deposits", ["account_id"])
op.create_index("ix_grid_deposits_asset", "grid_deposits", ["asset"])
op.create_index("ix_grid_deposits_status", "grid_deposits", ["status"])
op.create_index("ix_grid_deposits_created", "grid_deposits", ["created"])


def downgrade() -> None:
op.drop_index("ix_grid_deposits_created", table_name="grid_deposits")
op.drop_index("ix_grid_deposits_status", table_name="grid_deposits")
op.drop_index("ix_grid_deposits_asset", table_name="grid_deposits")
op.drop_index("ix_grid_deposits_account_id", table_name="grid_deposits")
op.drop_table("grid_deposits")
91 changes: 91 additions & 0 deletions alembic/versions/0018_x402_payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: 2026 AI Power Grid
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Add x402 reservation provenance and settlement receipts.

Revision ID: 0018
Revises: 0017
Create Date: 2026-07-27
"""

import sqlalchemy as sa

from alembic import op

revision = "0018"
down_revision = "0017"
branch_labels = None
depends_on = None


def upgrade() -> None:
with op.batch_alter_table("grid_reservations") as batch:
batch.add_column(
sa.Column(
"billing_source",
sa.String(length=16),
nullable=False,
server_default=sa.text("'credits'"),
),
)
batch.add_column(sa.Column("external_payer", sa.String(length=64), nullable=True))
batch.add_column(sa.Column("actual_micro", sa.BigInteger(), nullable=True))
batch.create_index("ix_grid_reservations_billing_source", ["billing_source"])
batch.create_index("ix_grid_reservations_external_payer", ["external_payer"])

op.create_table(
"grid_x402_payments",
sa.Column("job_id", sa.String(length=64), primary_key=True),
sa.Column("authorization_id", sa.String(length=64), nullable=False),
sa.Column("payer", sa.String(length=64), nullable=False),
sa.Column("network", sa.String(length=64), nullable=False),
sa.Column("asset", sa.String(length=64), nullable=False),
sa.Column("pay_to", sa.String(length=64), nullable=False),
sa.Column("authorized_micro", sa.BigInteger(), nullable=False),
sa.Column("settled_micro", sa.BigInteger(), nullable=True),
sa.Column("tx_hash", sa.String(length=80), nullable=True),
sa.Column("status", sa.String(length=16), nullable=False),
sa.Column("error", sa.String(length=255), nullable=True),
sa.Column(
"attempts",
sa.Integer(),
nullable=False,
server_default=sa.text("0"),
),
sa.Column("last_attempt", sa.DateTime(timezone=True), nullable=True),
sa.Column("created", sa.DateTime(timezone=True), nullable=False),
sa.Column("settled", sa.DateTime(timezone=True), nullable=True),
sa.CheckConstraint(
"authorized_micro > 0",
name="ck_grid_x402_positive_authorization",
),
sa.CheckConstraint(
"settled_micro IS NULL OR settled_micro > 0",
name="ck_grid_x402_positive_settlement",
),
sa.CheckConstraint(
"settled_micro IS NULL OR settled_micro <= authorized_micro",
name="ck_grid_x402_settlement_within_authorization",
),
sa.UniqueConstraint(
"authorization_id",
name="uq_grid_x402_payments_authorization_id",
),
sa.UniqueConstraint("tx_hash", name="uq_grid_x402_payments_tx_hash"),
)
op.create_index("ix_grid_x402_payments_payer", "grid_x402_payments", ["payer"])
op.create_index("ix_grid_x402_payments_status", "grid_x402_payments", ["status"])
op.create_index("ix_grid_x402_payments_created", "grid_x402_payments", ["created"])


def downgrade() -> None:
op.drop_index("ix_grid_x402_payments_created", table_name="grid_x402_payments")
op.drop_index("ix_grid_x402_payments_status", table_name="grid_x402_payments")
op.drop_index("ix_grid_x402_payments_payer", table_name="grid_x402_payments")
op.drop_table("grid_x402_payments")

with op.batch_alter_table("grid_reservations") as batch:
batch.drop_index("ix_grid_reservations_external_payer")
batch.drop_index("ix_grid_reservations_billing_source")
batch.drop_column("actual_micro")
batch.drop_column("external_payer")
batch.drop_column("billing_source")
51 changes: 48 additions & 3 deletions deploy/env.template
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,63 @@ GRID_HOLDER_MIN_AIPG=100000
GRID_HOLDINGS_TTL=600
GRID_ETH_PRICE_TTL=60

# Base deposit claims remain disabled while GRID_DEPOSITS_ENABLED=0. Both
# treasury addresses must be controlled and monitored before enabling.
# Base funding remains disabled while GRID_DEPOSITS_ENABLED=0. USDC is the
# launch rail. AIPG additionally requires its own switch plus a fresh,
# conservative operator price epoch. Direct ETH remains disabled until an
# explicit conversion policy is selected.
GRID_BASE_CHAIN_ID=8453
GRID_BASE_RPC=https://mainnet.base.org
GRID_USDC_CONTRACT=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
GRID_USDC_TREASURY=
GRID_ETH_TREASURY=
GRID_DEPOSIT_CONFIRMATIONS=3
GRID_DEPOSIT_MIN_MICRO=10000
GRID_USDC_MAX_DEPOSIT_MICRO=10000000000
GRID_USDC_ACCOUNT_DAILY_MICRO=25000000000
GRID_USDC_NETWORK_DAILY_MICRO=100000000000

GRID_AIPG_DEPOSITS_ENABLED=0
GRID_AIPG_TOKEN=0xa1c0deCaFE3E9Bf06A5F29B7015CD373a9854608
GRID_AIPG_TREASURY=
GRID_AIPG_DECIMALS=18
GRID_AIPG_CREDIT_PRICE_MICRO=0
GRID_AIPG_PRICE_EPOCH=
GRID_AIPG_PRICE_AS_OF=
GRID_AIPG_PRICE_VALID_UNTIL=
GRID_AIPG_PRICE_BLOCK=0
GRID_AIPG_PRICE_MAX_AGE_SECONDS=86400
GRID_AIPG_DEPOSIT_HAIRCUT_BPS=300
GRID_AIPG_MAX_DEPOSIT_MICRO=100000000
GRID_AIPG_ACCOUNT_DAILY_MICRO=100000000
GRID_AIPG_NETWORK_DAILY_MICRO=500000000

# Direct ETH is intentionally unavailable while this is "disabled".
# "swap_receipt" credits only actual canonical-USDC proceeds delivered by a
# linked-wallet ETH swap and is the production policy. "buffered" is a capped,
# operator-only oracle pilot and must not be exposed in the public Console.
GRID_ETH_CONVERSION_MODE=disabled
GRID_ETH_TREASURY=
GRID_ETH_DEPOSIT_HAIRCUT_BPS=100
GRID_ETH_MAX_DEPOSIT_MICRO=100000000
GRID_ETH_ACCOUNT_DAILY_MICRO=100000000
GRID_ETH_NETWORK_DAILY_MICRO=500000000
GRID_ETH_USD_FEED=0x71041dddad3595f9ced3dccfbe3d1f4b0a16bb70
GRID_ETH_USD_FEED_DECIMALS=8

# Accountless agent payments. Default-off and text/non-streaming only in the
# initial rail. The payer authorizes up to $1 USDC; Core settles actual
# grid-counted usage. Base mainnet requires CDP facilitator credentials.
GRID_X402_ENABLED=0
GRID_X402_NETWORK=eip155:8453
GRID_X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
GRID_X402_PAY_TO=
GRID_X402_MAX_AUTH_MICRO=1000000
GRID_X402_DEFAULT_MAX_TOKENS=4096
# A pre-settlement attempt without a durable receipt becomes manual_review
# after this interval. Core never blindly retries an ambiguous Permit2 nonce.
GRID_X402_STALE_SECONDS=900
CDP_API_KEY_ID=
CDP_API_KEY_SECRET=

# ════════════════════════════════════════════════════════════════════
# [OPTIONAL] Pricing and future multi-asset payout policy
# ════════════════════════════════════════════════════════════════════
Expand Down
3 changes: 3 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ for humans and agents.
- `architecture-migration/` - Flask-to-FastAPI/Redis-stream/worker migration
planning.
- `BLOCKCHAIN_INTEGRATION.md` - legacy/on-chain integration guide.
- `FUNDING_RAIL.md` - Base asset acceptance and x402 architecture.
- `FUNDING_CANARY_RUNBOOK.md` - dark deploy, real-money canary evidence, and
rollback gates.
- `V2.md` - v2 API/design notes.

## Local Contracts
Expand Down
Loading
Loading