AIP-49: WALLET — agentwallet/v1 (principal-owned multi-asset wallet)
A principal-owned multi-asset wallet primitive. Assets are declared ERC-20-style (ref/symbol/decimals/standard/peg/ruleSet) and partitioned into ERC-1410 tranches; value lives in an append-only event journal whose fold is the balance. A restriction lattice makes laundering structurally impossible and conversions no-arbitrage; an authorize policy engine separates passive (cost-of-existing) from active (chosen) spend and bounds delegation by lineage-min. All settlement rails (Stripe/PawaPay/x402/on-chain) are adapters behind a per-asset SettlementPort.
| Field | Value |
|---|---|
| AIP | 49 |
| Title | WALLET — agentwallet/v1 (principal-owned multi-asset wallet) |
| Status | Draft |
| Type | Schema |
| Domain | wallet.sh |
| Requires | AIP-1, AIP-2 |
| Prior art | ERC-20 (fungible token), ERC-1400 / ERC-1410 (partially-fungible / partitioned token) |
Abstract
This AIP defines a principal-owned, multi-asset wallet primitive and the
defineAsset(...) signature that declares the assets it holds.
A wallet belongs to a principal (user / org / guild / operator / agent — any
addressable identity). It holds one or more assets, each declared
ERC-20-style (ref, symbol, decimals, standard, optional peg, and a
ruleSet). Each asset is divided into partitions — ERC-1410 tranches that
share an asset's value but carry their own restriction, expiry, and spendable
categories. Value lives as lots inside partitions; the lots are a projection
(fold) of an append-only event journal that is the sole source of truth.
Three laws hold structurally rather than by runtime check:
- No laundering. Restrictions form a join-semilattice under union;
conversion accumulates restrictions at the
meet, never strips them. - No arbitrage. The convert graph admits no cycle whose rate product exceeds 1.
- Bounded delegation. A delegate's effective allowance is the element-wise minimum down its delegation chain (lineage-min) — it can only narrow.
Spend carries an intent: passive (the metered cost of an agent existing /
thinking — gated by balance + an operating cap) versus active (an economic
action the agent chose — gated by a discretionary envelope). This is the safety
boundary: an agent may operate freely without being able to move value out.
Motivation
A single mutable balance integer cannot express what agentic products already
need, and bolting features onto it produces drift:
-
Multiple assets, one wallet. Loyalty points, paid credits, currency balances, and (runtime) guild- or chain-issued tokens are different assets with different pegs, networks, and rules — not one number. ERC-20-style declaration gives each a precise identity;
decimalsalone retires the recurring ×100 unit-bug class. -
Feature- and cohort-fenced value. "Image-only trial credits", "promo credits that expire in 21 days" are partitions (tranches), and "image credit can't pay for text" is a missing capability on a partition, not an
if. -
Provenance and reconciliation. A regulator-grade ledger must replay: every spent unit traces to the grant — and ultimately the external payment — that funded it. An append-only journal whose fold equals the balance makes drift detectable and stops it.
-
Agent safety. The dangerous capability is not spending but moving value out. Separating passive operation from active, discretionary, bounded settlement is the load-bearing guardrail against drift and prompt injection.
-
Pluggable settlement. Stripe, PawaPay, x402, and on-chain rails are interchangeable adapters behind one port, chosen per asset — not branches in the wallet.
Design principles
-
Asset ⊃ partition ⊃ lot — no taxonomy above the asset. There is no "asset class". The
standardfield (internal | iso4217 | erc20 | spl) is the only discriminator, and it routes to a settlement/valuation adapter polymorphically — neverif standard === …. -
The journal is truth; balance is a fold. No cell is mutated. Lots and balances are replays of the event log. The reconciliation oracle asserts
fold(events) == stored lots == balancecontinuously. -
Two edges, never conflated.
coalescemerges lots WITHIN an asset (value-preserving, free);convertcrosses BETWEEN assets (peg/forex, no-arbitrage). Onlyconvertmay cross a peg. -
Restrictions only accumulate. The lattice
meetis union; conversion isburn + mintat the meet, so dust stays restricted and laundering is self-defeating. -
Spend intent is first-class.
passivevsactiveis the authorization boundary; the allowance budgets the two separately (operatingvsdiscretionary). -
Delegation narrows, never widens. Effective envelope = lineage-min over the delegation chain.
-
Purity via ports. The primitive performs zero I/O. Storage, settlement, rate, and clock are injected (
@agentproto/wallet/ports); hosts supply concrete adapters. -
Settlement is two-phase. Outbound value rides
reserve → pay → capture | release, so internal value is never burned before the external leg confirms.
Specification
Asset declaration
An AssetDeclaration is the registry-keyed (ref) unit of value. ref is a
unique UPPER_SNAKE string (convention <ORIGIN>_<SYMBOL>, or a bare ISO-4217
code for fiat). Full field schema in
resources/aip-49/draft/ASSET.schema.json.
asset {
ref GUILDE_CREDITS | GUILDE_POINTS | USD | ERC20_USDT | SPL_USDC
name · symbol
decimals minor-unit precision (2 = centi, 6 = USDC, 18 = most ERC-20)
standard internal | iso4217 | erc20 | spl (→ adapter, not a taxonomy)
chain? ethereum | solana (on-chain standards)
peg? { vs, source } (→ RatePort)
ruleSet { settleOut, spendableOn, convertEdges, transfer, custody? }
partitions? [ PartitionSpec, … ] (the partition catalog)
}Partitions are co-located on the asset (a partition always belongs to one
asset), so the asset registry IS the partition catalog: a shared common set plus
per-app extend.
defineAsset(...) MUST: validate ref against ^[A-Z0-9][A-Z0-9_]{1,79}$,
validate the declaration against the schema, reject a convert edge whose to
equals the asset's own ref, and return a frozen handle.
Partition + policy
A partition is a tranche within an asset, identified <asset>:<tranche>. Lots
in one partition coalesce freely. A partition carries a policy — the
unified lifecycle runtime — instead of special-casing restriction and expiry as
separate fields:
policy = [ rule, … ] an ordered, JSON-serialisable list
rule =
| { kind: "spendOn", categories } narrows payable categories
| { kind: "restrict", tags } stamps lattice tags (lineage)
| { kind: "expire", at: TimeSpec } hard cliff → spendable 0
| { kind: "decay", curve, ratePerDay } spendable = remaining × factor(t)
| { kind: "vest", cliff?, durationMs } unlocks over time
| { kind: "transfer", scope } soulbound | account | open
TimeSpec = { kind: "afterGrant", ms } | { kind: "absolute", at }evaluatePolicy(policy, ctx) folds the rules into { spendable, eligible, restriction, nextTransitionAt }: quantity factors compose multiplicatively,
eligibility is AND-ed, restriction tags fold via the lattice union, and
nextTransitionAt is the earliest instant the spendable amount changes on its
own (the sweep schedules on it). Relative TimeSpecs anchor on the lot's grant
instant, so two grants into the same partition carry independent clocks. The
legacy shorthand (restriction, narrowed spendableOn, defaultTtlMs) remains
valid as a fast path for the common rules.
This maps directly on-chain: a policy is the off-chain twin of an ERC-1400
transfer-restriction module plus rebasing math; a relative-expiry partition is
realised on-chain by stamping the deadline into the bytes32 partition id.
Journal + lots
Verbs (event_kind): mint · burn · reserve · capture · release · convert_in · convert_out · transfer. A convert emits a correlated convert_out +
convert_in pair atomically; a hold is reserve then capture (settle) or
release (return). fold(events, now) projects lots
{ original, remaining, reserved, expiresAt?, status }; spendable balance for an
asset = Σ(remaining − reserved) over live (active, unexpired) lots.
Coalescing (intra-asset spend)
A spend names asset + category + amount. Eligible lots are those whose asset
matches and whose (partition-narrowed) spendableOn covers the category.
Default order: restricted-first (spend constrained value before fungible),
then expiring-first, then oldest. An asset with spendableOn: [] (e.g.
loyalty points) has NO eligible spend lot — it can only convert.
Convert (inter-asset) + no-arbitrage
convert burns the source and mints the destination at the lattice meet:
out.restriction → in.restriction = meet(source, edge.addsRestriction). Amounts
scale across decimals. A conformant implementation MUST provide a
verifyNoArbitrage check (negative-cycle detection over -log(rate)) and gate
it in CI: no cycle of convert edges may have a rate product > 1.
Authorize + delegation
authorize(request) returns { ok } or { ok: false, reason }. passive is
gated by balance and the operating cap; active additionally requires
settleOut to be permitted and is bounded by the discretionary envelope
(amount cap, rate/window, counterparty allow-list, asset allow-list). Delegation
chains resolve by minEnvelope (lineage-min): caps take the minimum, allow-lists
intersect.
Ports
StoragePort (load/commit journal + lots, atomic AND serialized per account;
spendableBalance accessor), SettlementPort (charge inbound / pay
outbound, per asset), RatePort (universal rate oracle), ClockPort. All I/O is
confined to these.
Reference implementation
@agentproto/wallet
— pure (zero runtime/filesystem/HTTP), property-tested:
restriction-lattice (semilattice axioms), fold (replay invariance + hold
cycle), coalescing (restricted-/expiring-first, ineligibility of points),
convert (decimals scaling, restriction accumulation, no-arbitrage cycle
detection), authorize (intent gating, lineage-min). Exposes defineAsset via
the AIP-1 createDoctype scaffold.
Known limitations
- Held multi-currency (B) and on-chain-authority custody are declared in the model but not part of the reference cloud pipeline; convert-on-entry and custodial-mirror are the day-1 defaults.
- Runtime/DB-backed asset declarations (guild-minted tokens, arbitrary
deposited ERC-20s) extend the config-first registry with a DB loader; the
refis the shared key so call sites are stable. - Agent-to-agent netting is supported by the event substrate but unspecified here.
Resources
ASSET.schema.json— asset declaration frontmatter / inline schema.
Resources
Supporting artifacts for AIP-49. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →