AIC Integration
Technical specification for integrating the AIC token into the inference app — staking discounts, AIC-as-payment, burn mechanics, and access gating.
Current Architecture Baseline
Authentication
Users authenticate via SIWE (EIP-4361). Session stores only { address, chainId } — no private keys. Sessions are encrypted HTTP-only cookies via iron-session.
Inference Payment Flow
1. Client → POST /api/inference/estimate
← { estimatedCostUsdc, estimatedSeconds }
2. Client → POST /api/inference (no Payment-Signature header)
← 402 Payment Required
3. Client signs EIP-712 TransferWithAuthorization (EIP-3009)
4. Client → POST /api/inference (with Payment-Signature header)
a. Validate payment payload (Zod)
b. Verify with x402 facilitator
c. Settle on-chain USDC transfer
d. Run Replicate inference ← AFTER settlement
e. Upload to R2 (if media)
f. INSERT into inferences table
← 200 { id, output, status, metrics }
Pricing
src/lib/pricing.ts computes costs from time:
estimateCostUsdc(task, ratePerSec, overrideSeconds?)
actualCostUsdc(predictTimeMs, ratePerSec)Default rate: $0.001 USDC / second. The server queries historical average predict_time_ms per model for accurate estimates.
Tier 1 — Staking Discounts
Overview
- Hold AIC → stake it → get a lower inference rate. Stakers pay less per second of compute.
- Pay with AIC instead of USDC. AIC holders can route payment through AIC rather than stablecoin.
Both independently increase demand to hold AIC.
Required: AIC Staking Contract
A new AICStak.sol contract must be deployed. Minimum interface:
interface IAICStaking {
function stakedBalance(address account) external view returns (uint256);
function stake(uint256 amount) external;
function unstake(uint256 amount) external;
function withdraw() external;
}The address becomes NEXT_PUBLIC_AIC_STAKING_ADDRESS (client) and AIC_STAKING_ADDRESS (server).
Discount Tiers
| Staked AIC | Discount | Effective rate (at $0.001/s) |
|---|---|---|
| 0 | 0% | $0.001000 / sec |
| ≥ 1,000 | 10% | $0.000900 / sec |
| ≥ 10,000 | 20% | $0.000800 / sec |
| ≥ 50,000 | 35% | $0.000650 / sec |
| ≥ 200,000 | 50% | $0.000500 / sec |
Implementation: src/lib/pricing.ts
const DISCOUNT_TIERS = [
{ minStaked: 200_000, discount: 0.50 },
{ minStaked: 50_000, discount: 0.35 },
{ minStaked: 10_000, discount: 0.20 },
{ minStaked: 1_000, discount: 0.10 },
] as const;
export function discountMultiplier(stakedAic: number): number {
for (const tier of DISCOUNT_TIERS) {
if (stakedAic >= tier.minStaked) return 1 - tier.discount;
}
return 1;
}
export function discountedRate(baseRatePerSec: number, stakedAic: number): number {
return roundUsdc(baseRatePerSec * discountMultiplier(stakedAic));
}Implementation: src/lib/staking.ts (server-side)
The server reads staked balance on-chain via viem (no wagmi on the server):
export async function readStakedAic(
userAddress: Address,
chainId: number
): Promise<number> {
const env = getServerEnv();
if (!env.AIC_STAKING_ADDRESS) return 0;
const client = createPublicClient({ chain, transport: http(rpcUrl) });
const raw = await client.readContract({
address: env.AIC_STAKING_ADDRESS as Address,
abi: STAKING_ABI,
functionName: "stakedBalance",
args: [userAddress],
});
return Number(raw) / 1e18;
}On failure (RPC error, contract not deployed), returns 0 — user is not blocked.
Applying the Discount in the Route Handler
// In src/app/api/inference/route.ts
const stakedAic = await readStakedAic(auth.address as Address, chainId)
.catch((e) => { console.error("[inference] staking read failed:", e); return 0; });
const ratePerSec = discountedRate(parseFloat(env.INFERENCE_RATE_PER_SEC), stakedAic);
// All subsequent cost calculations use this discounted ratePerSecSchema Additions
// src/lib/db/schema.ts
stakedAicAtTime: numeric("staked_aic_at_time", { precision: 30, scale: 18 }),
discountPct: numeric("discount_pct", { precision: 5, scale: 2 }),
effectiveRateUsdc: numeric("effective_rate_usdc", { precision: 18, scale: 9 }),AIC-as-Payment
Design Constraints
The current x402 flow wraps USDC's EIP-3009 transferWithAuthorization. AIC implements EIP-2612 (permit) but not EIP-3009.
Recommended approach (Option B): Upgrade WebAI3Token to also implement EIP-3009, or deploy a thin wrapper. This keeps the x402 integration uniform — same path, different asset address.
Dual-Asset Payment Requirement
Extend src/lib/x402.ts to emit two entries in accepts[]:
accepts: [
{
scheme: "exact",
network,
asset: usdcConfig.address,
amount: usdcAmountOnChain.toString(),
payTo: env.RECEIVER_ADDRESS,
maxTimeoutSeconds: 60,
extra: { name: "USD Coin", version: "2" },
},
...(aicConfig ? [{
scheme: "exact",
network,
asset: aicConfig.tokenAddress,
amount: aicAmountOnChain.toString(),
payTo: env.RECEIVER_ADDRESS,
maxTimeoutSeconds: 60,
extra: { name: "WebAI3 Token", version: "1" },
}] : []),
],The client selects the preferred asset and signs the appropriate EIP-712 domain.
AIC Pricing
AIC is not a stablecoin. Rate sources (ordered by preference):
- On-chain TWAP from a Uniswap v3 AIC/USDC pool (most decentralized)
- Off-chain price API (CoinGecko, CoinMarketCap)
- Fixed admin-set rate via
AIC_USDC_RATEenv var (simplest for early rollout)
Use option 3 initially; graduate to on-chain TWAP once the liquidity pool is established.
Tier 2 — Burn-on-Use + Access Gating
Burn-on-Use
When a user pays in AIC, a configurable percentage (e.g., 20%) is burned permanently:
contract AICSplitter {
address public immutable treasury;
uint256 public immutable burnBps; // e.g. 2000 = 20%
function split(IERC20Burnable token, uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
uint256 burnAmount = amount * burnBps / 10_000;
token.burn(burnAmount);
token.transfer(treasury, amount - burnAmount);
}
}Note: This requires adding a
burn(uint256)function toWebAI3Token. The current contract does not have one. This is a new deployment or wrapper — the existing contract is immutable.
Burn rate controlled by AIC_BURN_BPS env var (integer 0–10000). Future: put on-chain for AIC governance vote.
Access Gating
Three model tiers with AIC requirements:
| Tier | AIC Required | Examples |
|---|---|---|
| Free | 0 AIC | Base Stable Diffusion, small LLMs |
| Standard | ≥ 500 AIC held | Mid-range diffusion, mid-size LLMs |
| Premium | ≥ 5,000 AIC staked | SDXL, large LLMs, high-quality audio |
"Held" = raw wallet balance (balanceOf). "Staked" = locked in staking contract (stakedBalance).
Gating Logic: src/lib/access.ts
const TIER_REQUIREMENTS = {
free: { heldAic: 0, stakedAic: 0 },
standard: { heldAic: 500, stakedAic: 0 },
premium: { heldAic: 0, stakedAic: 5_000 },
};
export function hasAccess(
tier: ModelTier,
heldAic: number,
stakedAic: number
): boolean {
const req = TIER_REQUIREMENTS[tier];
return heldAic >= req.heldAic && stakedAic >= req.stakedAic;
}The server is the authoritative gate — client-side gating is UI-only.
Rate Limit Tiers
const RATE_LIMITS = {
free: { windowMs: 60_000, max: 5 },
standard: { windowMs: 60_000, max: 20 },
premium: { windowMs: 60_000, max: 60 },
};Implementation Order
- Deploy the AIC staking contract (prerequisite for everything)
- Add discount tiers in
pricing.ts+ server-side staking read - Show AIC balance + stake info in the wallet UI
- Add model tier annotations + client-side lock UI (UI only, no enforcement yet)
- Deploy on testnet, validate discount math with real staked balances
- Enforce gating server-side once tier assignments are finalized
- Add EIP-3009 support to
WebAI3Tokenfor AIC payments - Update x402 flow to support dual-asset
accepts[] - Deploy splitter contract, wire
AIC_BURN_BPS - Add
aicBurnedAmountto stats API and surface on landing/dashboard
Environment Variables (Complete Post-Integration)
Client
NEXT_PUBLIC_ETHEREUM_RPC_URL
NEXT_PUBLIC_SEPOLIA_RPC_URL
NEXT_PUBLIC_AIC_TOKEN_ADDRESS # AIC token address
NEXT_PUBLIC_AIC_STAKING_ADDRESS # Staking contract address
Server
REPLICATE_API_TOKEN
DATABASE_URL
SESSION_SECRET
RECEIVER_ADDRESS
INFERENCE_RATE_PER_SEC # Base rate, default 0.001
X402_FACILITATOR_URL
AIC_TOKEN_ADDRESS # Server-side balance reads
AIC_STAKING_ADDRESS # Server-side staked balance reads
AIC_USDC_RATE # AIC per 1 USDC (e.g. "10" = $0.10/AIC)
AIC_BURN_BPS # Basis points burned on AIC payment