Docs / Onchain

Onchain

Addresses, instructions and events, and how to check the reserve and any request.

The Unspent program runs on Solana. It holds the USDC behind every credit, keeps every API balance and records every request.

Addresses

Program5YSrsYtX6UnSx8KAbiD8RWM7UzNNXU3w5QhxSA2vvfkx
ConfigC789cN7uBANfyb9eysnidu6WxKoKukXTp5t28brLR7tv
Credit mint4zJouF8kpccTavYkBnbWkt6xF6Qni1xqqC3oegSG34rM
Reserve (USDC)F5VJ6EpiTaPdW8hRMeReznMm4J9QNdxQXaMJoPEWmhLG
Vault (API balances)9F641P9ZV7zQtsDddG7hbA7CJKmUX7mdCFCH3VpyFHho
Treasury6DDyY4bVh1W7rURPB9qmCwdUKmN8StHuZMN5REruyTmM
USDC mintEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

Accounts

AccountSeedsHolds
Config"config"Limits, pause flags, totals, the admin, gateway and treasury keys
User account"user", ownerbalance, reserved and the hold limit of one wallet
Reservation"res", request idOne request's hold: owner, amount, expiry, rent payer
Reserve"reserve"USDC token account behind the credits in circulation
Vault"vault"Credit token account holding all API balances

All of them are program-derived addresses of the program above.

Instructions

InstructionWho signsWhat it does
buy_creditsBuyerMoves USDC into the reserve and mints the same number of credits to the buyer
depositOwnerMoves credits from the wallet into the vault
withdrawOwnerMoves available credits back to the wallet. Closes expired holds passed with it
set_my_reserve_capOwnerSets the wallet's own hold limit
redeemAny holder, after shutdownBurns credits and pays the same amount of USDC from the reserve
reserveGatewayHolds a request's worst-case cost
settle, settle_manyGatewayBurns the actual cost, at most the hold, moves the same amount of USDC from the reserve to the treasury and releases the rest
expireAnyoneReleases a hold after it expires
apply_pendingAnyoneApplies a delayed change once its time has come
set_limitAdminLowers a limit at once. Raising it is a delayed change
set_pauseAdminPauses purchases, deposits or the gateway
propose_treasury, propose_gateway_backup, cancel_pendingAdminDelayed changes of the treasury and the backup gateway key
revoke_gateway, activate_backup_gatewayAdminRemoves a compromised gateway key, switches to the approved backup key
shutdownAdminStops purchases, deposits and new holds for good and opens redeem
sweep_surplusAdminMoves USDC above the credits in circulation to the treasury

What the admin and the gateway can't do

  • Move credits out of API balances or USDC out of the reserve other than through the instructions above. The gateway can only burn what a request it held settles for, never more than the hold.
  • Block withdrawals or expiry. Neither needs anyone's approval, and pauses don't stop them.
  • Make a change fast. Raising a limit, changing the treasury, the backup gateway key or the delay itself takes 48 hours and is visible onchain as a pending change from the start. Withdrawals are instant, so everyone has time to leave before a change applies.
  • Sweep more than the surplus. sweep_surplus takes only USDC above the credits in circulation, for example USDC left behind by credits someone burned themselves. It never touches balances or holds.

Program upgrades

Every credit is backed by 1 USDC as long as the program's code is the code described here. Until upgrades are frozen, the upgrade authority can replace that code, so the backing also depends on whoever holds that key.

Upgrade authority right now: held by 9PeMBb8kkpRDwRDEQYREgQkqkJKKmbMx1wXzFmoyHNcM.

Check the reserve

  • Credits in circulation: the supply of the credit mint.
  • USDC in reserve: the balance of the reserve account.

The reserve is never smaller than the supply. /reserve shows both live with the coverage, totals and the latest settlements. Any Solana explorer shows the same numbers.

Check a request

Every API response carries x-unspent-request-id: the request's 32-byte id in hex. Its reservation address is derived from it, and that address's history holds the request's two transactions: reserve and then settle (or expire if it never settled).

ts
import { createSolanaRpc, getProgramDerivedAddress } from "@solana/kit";

const requestId = "3f9c0e…"; // x-unspent-request-id
const [reservation] = await getProgramDerivedAddress({
  programAddress: "5YSrsYtX6UnSx8KAbiD8RWM7UzNNXU3w5QhxSA2vvfkx",
  seeds: ["res", Uint8Array.from(Buffer.from(requestId, "hex"))],
});
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const history = await rpc.getSignaturesForAddress(reservation).send();

The reserve transaction logs a Reserved event with the hold. The settle transaction logs a Settled event with actual: what the request cost. Settlements are batched, so one settle_many transaction can settle several requests. /app/usage links both transactions of every request.

Events

Events are Anchor events in the program's logs. Amounts are in base units: 1 credit is 1,000,000.

EventFields
CreditsBoughtbuyer, amount
Depositedowner, amount
Withdrawnowner, amount
ReserveCapSetowner, cap
Reservedowner, request_id, amount, expires_at
Settledowner, request_id, reserved, actual
Expiredowner, request_id, amount
Redeemedowner, amount
ShutDownnone
SurplusSweptusdc, credits
LimitChangedfield, value
PauseSettarget, paused
ChangeProposedfield, value, key, effective_at
ChangeAppliedfield, value, key
ChangeCancelledfield
GatewayRevokednone
BackupGatewayActivatedgateway