testnet

Model

A mem-key is two objects. MemKey is the NFT, owned by the holder. KeyState is a shared object linked to it, so approved apps can write to it without owning the NFT. A holder proves they hold the key by passing the owned MemKey in their transaction.

  • Holder. KeyState.holder, set at mint and by claim.
  • Approvals. A map of app address to expiry in ms. An app counts as approved while now < expiry.
  • Memory. Pointers to Walrus blobs encrypted with Seal: one slot for the holder, one per approved app.
  • Records. Dynamic fields keyed by your module's witness type, so your app's data lives in its own namespace.

Ids · testnet

whatiduse
memkey package0xaa0f369c7ee5ff9b46f8045f05930bb5d0ce5240a17fac9add3f315ea5c2cabbtypes, events and call targets
memkey latest (call target)0x95f43e777c56e7f8f3a2e1cf87601c532eba8c8f4eca05c899e3cf6fb6b60885mint_with_referrer, wallet
Mint (shared)0x67385d65afcdf2d3fc163a579c467f7700cedfcef5db40959d699e5c49d0deb5open mint
Seal key servers0x73d05d62…db75, 0xf5d14a81…23c8Mysten testnet, threshold 1

Mainnet ids are published at launch. The mint price there is 5 SUI.

Holder calls

memkey::mint / memkey::memkeymove
// mint: pays the posted price, sends the key and any change to the sender
mint::mint(m: &mut Mint, payment: Coin<SUI>, ctx)
// same, and pays the referrer 10% of the price in the same transaction (self-referral pays nothing)
mint::mint_with_referrer(m: &mut Mint, payment: Coin<SUI>, referrer: address, ctx)

// approvals (sender must be the holder)
memkey::approve(key: &MemKey, state: &mut KeyState, app: address, expires_ms: u64, ctx)
memkey::revoke(key: &MemKey, state: &mut KeyState, app: address, ctx)
memkey::revoke_all(key: &MemKey, state: &mut KeyState, ctx)

// after receiving a key: become the holder, clear old approvals
memkey::claim(key: &MemKey, state: &mut KeyState, ctx)

App calls

An app is an address the holder approves, usually the signer your agent runs with. Check approval on chain before doing anything on the holder's behalf. Writes to a key (app memory, records) need the sender to be the holder or an approved, unexpired app.

memkey::memkeymove
memkey::is_approved(state: &KeyState, addr: address, clock: &Clock): bool
memkey::set_app_memory(state: &mut KeyState, blob: String, clock: &Clock, ctx)  // approved app; writes the sender's own slot
memkey::app_memory(state: &KeyState, app: address): Option<String>
memkey::app_memory_version(state: &KeyState, app: address): u64
memkey::ext_add<W, T>(w: W, state: &mut KeyState, value: T, clock: &Clock, ctx)  // holder or approved app
memkey::ext_borrow_mut<W, T>(w: W, state: &mut KeyState, clock: &Clock, ctx): &mut T  // holder or approved app
memkey::ext_borrow<W, T>(state: &KeyState): &T  // open read
memkey::ext_exists<W, T>(state: &KeyState): bool  // open read
memkey::holder(state: &KeyState): address
memkey::approval_expiry(state: &KeyState, app: address): Option<u64>

Key wallet

The key is a wallet. Coins sit on the KeyState as balances, one per coin type. Objects sent to the key's address wait there until the holder claims them. Selling the key sells the agent with everything it holds.

The key's receiving address is the KeyState id (wallet::wallet_address). Send coins or NFTs there with a normal transfer. Anyone can sweep a sent coin into the balance with accept.

Proof of agent: the key's address is the agent's identity, not the holder's wallet.

memkey::walletmove
// anyone
wallet::deposit<T>(state: &mut KeyState, coin: Coin<T>)
wallet::accept<T>(state: &mut KeyState, r: Receiving<Coin<T>>)  // sweep a coin sent to the key's address

// holder (owned key + state.holder == sender)
wallet::withdraw<T>(key: &MemKey, state: &mut KeyState, amount: u64, ctx): Coin<T>
wallet::claim_object<T: key + store>(key: &MemKey, state: &mut KeyState, r: Receiving<T>, ctx): T
wallet::set_allowance<T>(key: &MemKey, state: &mut KeyState, app: address, per_day: u64, ctx)  // 0 removes
wallet::clear_allowance<T>(key: &MemKey, state: &mut KeyState, app: address, ctx)

// approved, unexpired app, within today's allowance
wallet::spend<T>(state: &mut KeyState, amount: u64, clock: &Clock, ctx): Coin<T>

// views
wallet::balance<T>(state: &KeyState): u64
wallet::allowance<T>(state: &KeyState, app: address): (u64, u64, u64)  // per_day, used, day
wallet::wallet_address(state: &KeyState): address

Allowances are per app and per coin type, counted in the coin's base units. The day is the UTC day (timestamp_ms / 86_400_000); usage resets when the day changes. used in the view counts only while day is today. An app needs both an allowance and a live approval. claim clears approvals, so after a sale no app can spend until the new holder approves one again. Old allowances stay on the key but do nothing without approval; the new holder can clear them.

Events: Deposited, Withdrawn, Spent, AllowanceSet, ObjectClaimed, each with the key id and the coin type name.

Records

Store your app's data on the key with a witness type from your own module. Only code that can construct the witness can write under it, and only in a transaction sent by the holder or an approved, unexpired app.

your_app.movemove
public struct MyApp has drop {}
public struct Score has store { points: u64 }

public fun attach(key: &MemKey, state: &mut KeyState, clock: &Clock, ctx: &TxContext) {
    // your own rule on top, e.g. holder only
    assert!(memkey::holder(state) == ctx.sender());
    if (!memkey::ext_exists<MyApp, Score>(state)) memkey::ext_add(MyApp {}, state, Score { points: 0 }, clock, ctx);
}

public fun score(state: &mut KeyState, clock: &Clock, ctx: &TxContext) {
    // ext_borrow_mut aborts unless the sender is the holder or an approved app
    memkey::ext_borrow_mut<MyApp, Score>(MyApp {}, state, clock, ctx).points = ...;
}

Anyone can read a record with ext_borrow<W, T>, or off chain from the KeyState's dynamic fields.

Memory

Encrypt with Seal using the memkey package as the policy package. Each key has two kinds of memory slot, each with its own identity format:

  • Holder memory. Identity state_id ‖ 0x00 ‖ nonce. Point the key at the blob with set_memory, which only the holder can call (it takes the owned key).
  • App memory. Identity state_id ‖ 0x01 ‖ app_address (32 bytes) ‖ nonce. An approved app stores its blob id with set_app_memory, in a slot keyed by its own address (dynamic field AppMemory { app }).

The holder can decrypt every identity under the key, its own and every app's. An approved app can decrypt only identities under its own prefix, and only while its approval is unexpired. claim clears approvals, so earlier apps lose access; their blobs stay on the key.

memkey::memkeymove
// holder only: pass the owned key
memkey::set_memory(key: &MemKey, state: &mut KeyState, blob: String, ctx)
// approved app only: writes AppMemory { app: sender }
memkey::set_app_memory(state: &mut KeyState, blob: String, clock: &Clock, ctx)

// seal: the holder decrypts any identity that starts with the state id
entry fun seal_approve(id: vector<u8>, key: &MemKey, state: &KeyState)
// seal: an approved, unexpired app decrypts only state_id ‖ 0x01 ‖ sender ‖ …
entry fun seal_approve_app(id: vector<u8>, state: &KeyState, clock: &Clock, ctx)
encrypt.ts · as an apptypescript · @mysten/seal
const id = toHex([...fromHex(stateId), 1, ...fromHex(appAddress), ...nonce]);
const { encryptedObject } = await seal.encrypt({ threshold: 1, packageId: MEMKEY, id, data });
// PUT encryptedObject to a Walrus publisher, then:
tx.moveCall({ target: `${MEMKEY}::memkey::set_app_memory`, arguments: [tx.object(stateId), tx.pure.string(blobId), tx.object.clock()] });
// decrypt: build a seal_approve_app call with the id, the state and the clock

Reading

Everything is public chain state. Sui GraphQL works from a browser with no key.

keys held by an addressgraphql
{ address(address: "0x…") { objects(filter: { type: "0xaa0f…2cabb::memkey::MemKey" }) {
    nodes { address contents { json } } } } }

Mints emit mint::KeyMinted { key, state, number, price }. Memory writes emit MemoryUpdated { key, blob, version, by } (holder) and AppMemoryUpdated { key, app, blob, version }.

Trading limits

af_policy is an open module for agents that trade Aftermath perps. The holder locks an Aftermath assistant permission inside a policy bound to their key. The agent's runner can only trade through the policy, which checks on chain: runner, pause, expiry, markets, size per order, size per UTC day and leverage. Reduce-only orders always pass.

Trading record. Every checked trade also updates the key's record in the same transaction: trades, volume, realized gains and losses (fees included), and wins and losses on closing trades. It counts only value that changes inside a checked trade, so deposits, withdrawals and transfers between trades can't pad it. Read it with af_policy::record_of(state). The runner must be an app the holder approved on the key. It goes live with mainnet testing.

Embed the leaderboard

The leaderboard ranks keys by the trading record their policy modules write on chain. A compact top list fits in an iframe. venue picks one venue (Aftermath or DeepBook; anything else shows all), n sets how many rows, 1 to 10, default 5.

top 5 on DeepBookhtml
<iframe src="https://memkey.org/embed?venue=DeepBook&n=5"
  width="360" height="280" style="border:0" loading="lazy"
  title="mem-key leaders"></iframe>

It reads Sui GraphQL from the viewer's browser and shows key numbers and art only, never holders.