Access Gate — developer integration
How to integrate with access_gate on-chain: building transactions, reading state, verifying passes, and consuming single-use passes replay-safely. For every function, type, event and abort code see the API reference.
Package (testnet): 0xd7ddaa94b74330979b2b618fc81206d160a264f1c9ca148a77fa2144301388c9, PlatformConfig0x3f81489df58233d96798f34ad44de325e1b4df7e4d615affe72ca181969ee7b5. The TypeScript client is @meddleware/access-gate-client (reads, events, builders, abort messages, and these ids per network in its deployments export); the examples below use @mysten/sui directly so the on-chain contract is explicit. PKG stands for both ids, which are equal until the package is upgraded: transactions call the latest published-at id, while types and events are matched at the original id. The gateway challenge/proof wire format is in @meddleware/access-gate-gateway-client.
Normative integration rules
These are requirements, not suggestions. They mirror the package audit's normative section.
Consume MUST be called by the pass holder with the NFT by value.
consume/consume_soulboundtake the NFT object itself, so only the owner's transaction can supply it. Never design a flow where a third party is expected to spend a user's pass.A consumption is replay-proof only when the verifier enforces it. The chain checks that the nonce is at least 8 bytes and records
consumer = tx sender; it does not enforce nonce uniqueness or freshness. A verifier that grants access based onAccessConsumedEventMUST use the digest-first binding (what both reference gateways implement):- read the consume transaction by its digest from a finalised (checkpointed) transaction, never from a client-supplied payload;
- accept it only if it holds the exact event type
<originalId>::access_gate::AccessConsumedEvent, the transaction sender (the event'sconsumer) equals the address that signed the access proof, andgate_idequals the gate being accessed; - have the signed access message name the verifier's own audience and the digest, so a signature cannot be replayed at another verifier or for another consumption;
- redeem each digest once (a lease, then commit or release in a redemption store), and bound the age of an accepted consumption to the full node's retention window.
The event
nonceis carried for a verifier that also issues challenges and may match it; the digest-first protocol does not depend on it (which lets an interrupted upload resume with a fresh challenge).Verify the pass against the gate on-chain. Use
is_valid_for/is_valid_for_soulbound(or compareAccessData.gate_id) — never trust a client's claim about which gate a pass belongs to.Handle both NFT types. A gate mints either
AccessNFTorSoulboundAccessNFT(seegate_is_soulbound); call the matching function variant.Route to the package that minted the pass. Within one upgrade lineage, always call the latest
published-at(older versions abort aftermigrate). A pass of a different package (another original id) can only be consumed by that package'sconsume: subscribe to events of every original id you trust.
Creating a gate (PTB)
Every gate is created with an immutable GatePolicy, built in the same PTB:
const [policy] = tx.moveCall({
target: `${PKG}::access_gate::new_gate_policy`,
arguments: [
tx.pure.bool(freezeRequiresUnpaused), tx.pure.bool(lockCommissionOnFreeze),
tx.pure.bool(pauseBlocksDecryption), tx.pure.bool(pauseBlocksAccess),
],
})
// Paid gate: price must be ≥ min_paid_price_mist(platform) (0.005 SUI by default), else abort 11.
tx.moveCall({
target: `${PKG}::access_gate::create_gate`,
arguments: [tx.object(PLATFORM_CONFIG_ID), tx.pure.u64(priceMist), /* recipient, uses, soulbound,
auto-burn, name, image url, description */ ...values, policy],
})
// Free gate instead: pay the platform's free_gate_fee_mist.
// const [fee] = tx.splitCoins(tx.gas, [tx.pure.u64(freeGateFeeMist)])
// tx.moveCall({ target: `${PKG}::access_gate::create_free_gate`,
// arguments: [tx.object(PLATFORM_CONFIG_ID), fee, ...values, policy] })@meddleware/access-gate-client's buildCreateGateTx(publishedAt, platformConfigId, { …, policy, freeGateFeeMist }) does this; read the terms with fetchPlatformConfig. Admin calls that depend on the platform terms — set_price, make_gate_free, airdrop (pays the commission) and make_gate_immutable — take the shared PlatformConfig; the client builders read it from GateAdminContext.platformConfigId.
Buying a pass (PTB)
import { Transaction } from '@mysten/sui/transactions'
const tx = new Transaction()
const [payment] = tx.splitCoins(tx.gas, [tx.pure.u64(priceMist)])
tx.moveCall({
target: `${PKG}::access_gate::purchase`,
arguments: [tx.object(GATE_ID), tx.object(PLATFORM_CONFIG_ID), payment],
})
// Overpayment is refunded to the sender; the NFT is minted to the sender.purchase aborts with E_PAUSED (1) if the gate is paused and E_INSUFFICIENT_PAYMENT (2) if the coin is worth less than gate_price_mist.
Consuming a single-use pass (PTB)
const tx = new Transaction()
tx.moveCall({
target: `${PKG}::access_gate::${soulbound ? 'consume_soulbound' : 'consume'}`,
arguments: [tx.object(NFT_ID), tx.object(GATE_ID), tx.object(PLATFORM_CONFIG_ID), tx.pure.vector('u8', nonceBytes)],
})PKG must be the latest published-at (take it from @meddleware/access-gate-client's deployments; an older one aborts E_WRONG_VERSION after a migrate), and nonceBytes at least 8 bytes.
The transaction emits AccessConsumedEvent { nft_id, gate_id, nonce, consumer, uses_after, timestamp_ms }. If uses_after == 0 and the gate has auto_burn_at_zero, the NFT is deleted and an AccessBurnedEvent follows; otherwise the NFT is returned to the sender.
Reading state (gRPC)
import { SuiGrpcClient } from '@mysten/sui/grpc'
const client = new SuiGrpcClient({ network: 'testnet', baseUrl: 'https://fullnode.testnet.sui.io:443' })
// A wallet's passes for one gate (filter by type, then by gate_id in the object JSON).
const { objects } = await client.listOwnedObjects({
owner, type: `${PKG}::access_gate::SoulboundAccessNFT`, include: { json: true },
})gRPC renders struct fields flat (json.data.gate_id) and may render framework addresses in long form — match types tolerantly.
Error handling
Abort codes are unique within access_gate (see the API reference). Other packages reuse the same small integers (e.g. sealed_storage::nft_gate also uses 1–3), so always key error messages on (module, code), never on the code alone.
Versioning
Published versions are intended to be immutable; a new version is a new package address. Existing gates and passes remain valid under the version that created them — keep supporting every address whose gates are still in use.
Canonical source: @meddleware/access-gate-sui → docs/onchain/dev-guide.md — maintained with the Move package; edit it there, not on this site.