This document describes the Enoki integration in the project for sponsored transactions: the user never pays gas; the application (via the Enoki API key) pays.
- What is Enoki?
- Sponsored Flow Overview
- Architecture in the Project
- Detailed Flow (Code)
- Configuration
- Allowed Addresses
- Usage in the App
- Troubleshooting
- References
- Enoki (Mysten Labs) provides:
- ZkLogin: sign-in with Google / Twitch / etc. and derivation of a SUI address.
- Sponsored transactions: another address (the “sponsor”) pays gas on behalf of the user.
In this project, Enoki is used mainly for gas sponsoring: all on-chain actions (create profile, add tier, subscribe, publish post, etc.) go through a sponsored tx. The user signs the transaction but does not spend SUI on gas.
- Official docs: docs.enoki.mystenlabs.com
- HTTP API: docs.enoki.mystenlabs.com/http-api/openapi
- Frontend: build the transaction without a gas block (
onlyTransactionKind: true). - Backend: send this tx + the sender address to the Enoki Sponsor API. Enoki adds gas (paid by the account tied to your API key) and returns a full tx (bytes + digest).
- Frontend: the user signs these bytes with their wallet (they sign as sender, not as gas payer).
- Backend: send the signature + digest to the Enoki Execute API. Enoki submits the tx to the network; gas is debited from Enoki, not the user.
On-chain result: sender = user, gas payer = Enoki account.
Frontend (app/)
├── enoki/sponsor/
│ ├── useSponsoredTransaction.ts ← Main hook: sponsorAndExecute(tx)
│ ├── createEnokiClient.ts ← Enoki client (server-side, execute)
│ └── index.ts
├── app/api/enoki/
│ ├── sponsor/route.ts ← POST: receives tx + sender → calls Enoki sponsor
│ └── execute/route.ts ← POST: receives digest + signature → Enoki execute
└── lib/
├── contract-constants.ts ← ALLOWED_MOVE_CALL_TARGETS, ALLOWED_ADDRESSES
└── contract.ts ← Tx builders (buildCreateProfile, buildSubscribe, …)
- No user tx should be signed/executed outside this flow for sponsored actions (no direct
signAndExecuteTransactionwith user gas). - Tx builders in
lib/contract.tsmust not usetx.gas; gas is always added by Enoki.
The transaction is built without gas:
const txBytes = await transaction.build({
client,
onlyTransactionKind: true,
});
const transactionKindBytes = toBase64(txBytes);POST /api/enoki/sponsor with e.g.:
{
"transactionKindBytes": "<base64>",
"network": "testnet",
"sender": "0x...",
"extraAllowedAddresses": ["0x..."]
}The route app/api/enoki/sponsor/route.ts:
- Reads
transactionKindBytes,sender,extraAllowedAddresses. - Calls
POST https://api.enoki.mystenlabs.com/v1/transaction-blocks/sponsorwith:transactionBlockKindBytessenderallowedMoveCallTargets(fromcontract-constants.ts)allowedAddresses=ALLOWED_ADDRESSES+sender+extraAllowedAddresses
- Enoki returns
bytes(full tx with gas) anddigest.
The user signs the returned bytes (already “sponsored” tx):
const { signature } = await signTransaction({ transaction: bytes });POST /api/enoki/execute with { digest, signature }. The route execute/route.ts calls the Enoki client:
await client.executeSponsoredTransaction({ digest, signature });Enoki then submits the tx to the network; gas is debited from the Enoki account.
| Variable | Side | Description |
|---|---|---|
ENOKI_PRIVATE_API_KEY |
Server | Enoki private API key (dashboard). Never expose on the client. |
The key is used in:
app/api/enoki/sponsor/route.ts(HTTP call to Enoki sponsor API).app/enoki/sponsor/createEnokiClient.ts(client forexecuteSponsoredTransaction).
In the Enoki portal, you configure Allowed Move Call Targets: only the listed Move functions can be sponsored.
For this project, the list must include at least the entry points actually called by the frontend via sponsorAndExecute:
service::create_creator_profileservice::update_creator_profileservice::delete_creator_profileservice::add_subscription_tierservice::remove_subscription_tierservice::publish_postservice::update_postservice::set_post_visibilityservice::delete_postservice::withdraw_creator_fundssubscription::subscribe
Format: {PACKAGE_ID}::module::function (e.g. 0x...::service::create_creator_profile).
The list in code (lib/contract-constants.ts — ALLOWED_MOVE_CALL_TARGETS) is sent with every sponsor request; it should match the dashboard config (depending on Enoki mode, the dashboard may restrict or be the source of truth).
Enoki only accepts transactions that use allowed objects/addresses. We send:
- allowedAddresses =
ALLOWED_ADDRESSES(Platform, Clock, etc.) + sender + extraAllowedAddresses.
Important case: Subscribe. The tx uses the user’s SUI coins to pay for the tier. Those coin object IDs must be allowed, or Enoki rejects the tx. Hence:
- The frontend fetches the user’s coins (
getCoins). - It builds the tx passing those coins (not
tx.gas). - It calls
sponsorAndExecute(tx, { extraAllowedAddresses: [ ...coinIds ] }).
The /api/enoki/sponsor route merges extraAllowedAddresses with the base addresses before calling the Enoki API.
import { useSponsoredTransaction } from "@/enoki/sponsor";
const { sponsorAndExecute, isPending, error } = useSponsoredTransaction();
// Tx without user coins (e.g. create profile, add tier)
await sponsorAndExecute(buildCreateProfile(name, description));
// Tx with user coins (e.g. subscribe)
const coins = await suiClient.getCoins({ owner: address, coinType: "0x2::sui::SUI" });
const tx = buildSubscribe(..., selectedCoins);
await sponsorAndExecute(tx, { extraAllowedAddresses: selectedCoins.map(c => c.coinObjectId) });- Never use
tx.gasin sponsored tx builders — gas is provided by Enoki. - If the tx spends objects owned by the user (coins, NFTs, etc.), pass their IDs in extraAllowedAddresses.
- Any new Move entry point called via
sponsorAndExecutemust be added toALLOWED_MOVE_CALL_TARGETSand, if required, to the “Allowed Move Call Targets” config in the Enoki dashboard.
| Issue | What to check |
|---|---|
Sponsor failed (502) / API error |
Verify ENOKI_PRIVATE_API_KEY, quotas, and Enoki status. |
| Transaction rejected by Enoki | Ensure the called Move function is in Allowed Move Call Targets and all addresses/objects used are in allowedAddresses (including extraAllowedAddresses for coins). |
| “Insufficient gas” or gas-related error | Ensure no builder uses tx.gas and the tx is built with onlyTransactionKind: true before sending to sponsor. |
| Subscribe fails | Ensure the coin objects used for payment are passed in extraAllowedAddresses. |