IOTA Gas Station Workshop
In this hands-on workshop you will build Spark Mini, a fully functional, decentralized microblogging platform where anyone can post messages without owning a single token or paying gas fees.
Learning Objectives
By the end of this workshop, you will:
- Understand sponsored transactions and how the Gas Station enables gas-free UX.
- Deploy a local Gas Station and fund it for sponsorship.
- Build a React frontend with IOTA dApp Kit for wallet connection and UI.
- Implement on-chain posting using the IOTA TypeScript SDK.
- Integrate the full sponsored flow: reserve gas, user sign, Gas Station co-sign/submit.
- Test and debug your dApp with real testnet transactions.
Why IOTA Gas Station?
In traditional blockchains, users must buy tokens, fund wallets, and pay gas fees for every action, like posting a message. This creates massive friction, preventing adoption. IOTA Gas Station solves this by letting dApp operators cover fees, so users focus on creating content. Spark Mini demonstrates this with a Twitter-like platform: posts are immutable on IOTA testnet, but completely free.
Prerequisites
Before starting, ensure you have:
- Basic Knowledge: Familiarity with JavaScript/TypeScript, React, and command line. No prior IOTA experience needed.
- Tools:
- Node.js (v18+) and npm/yarn.
- Git.
- Docker Desktop (for Gas Station deployment).
- VS Code (recommended).
- Hardware: Mac/Linux/Windows with 4GB+ RAM.
If you're missing anything, install now:
Part 1 – Project Setup & Architecture
We'll build a full-stack dApp:
- Frontend (
spark-mini-frontend): React + Vite + dApp Kit for UI, wallet connect, and tx submission. - Gas Station (official Docker): Sponsors fees; no custom backend needed for this workshop.
- On-Chain Logic: Uses a demo Move package for storing posts (immutable on testnet).
1.1 Create the Workshop Folder
mkdir spark-mini-workshop && cd spark-mini-workshop
Explanation
This creates a dedicated folder for the entire workshop. All code and services will live here.
1.2 Clone and Launch the Official IOTA Gas Station
git clone https://github.com/iotaledger/gas-station.git
cd gas-station
../utils/./gas-station-tool.sh generate-sample-config --config-path config.yaml --docker-compose -n testnet
GAS_STATION_AUTH=supersecret123 docker compose up -d
Explanation
We are deploying the official, production-ready Gas Station using Docker. The generate-sample-config tool creates a testnet-ready configuration. The environment variable sets the auth token to supersecret123.
1.3 Verify the Gas Station is Healthy
curl http://localhost:9527/health
Expected output:
{"status":"healthy"}
Explanation
A successful response confirms the RPC server is listening on port 9527 and ready to sponsor transactions.
1.4 Create the React Frontend
cd ..
npx create-vite@latest frontend --template react-ts
cd frontend
npm install
npm install @iota/dapp-kit @iota/iota-sdk @tanstack/react-query axios
Explanation
We scaffold a modern Vite + React + TypeScript project and install:
- @iota/dapp-kit – wallet connection and hooks
- @iota/iota-sdk – transaction building
axios– HTTP calls to the Gas Station
1.5 Start the Dev Server
npm run dev
Explanation
Your frontend is now live at http://localhost:5173. You’ll see a blank page for now, that’s expected.
Part 2 – Wallet Connection with dApp Kit
2.1 Replace src/main.tsx (Providers)
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import {
createNetworkConfig,
IotaClientProvider,
WalletProvider,
} from '@iota/dapp-kit';
import { getFullnodeUrl } from '@iota/iota-sdk/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import '@iota/dapp-kit/dist/index.css';
const { networkConfig } = createNetworkConfig({
testnet: { url: getFullnodeUrl('testnet') },
});
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<IotaClientProvider networks={networkConfig} defaultNetwork="testnet">
<WalletProvider>
<App />
</WalletProvider>
</IotaClientProvider>
</QueryClientProvider>
</React.StrictMode>
);
Explanation
These providers are mandatory for any dApp Kit hook to work (useWallets, useSignTransaction, etc.). The CSS import gives us the beautiful default ConnectButton styling.
2.2 Basic UI with ConnectButton
// src/App.tsx (replace everything)
import React from 'react';
import { ConnectButton, useWallets } from '@iota/dapp-kit';
export default function App() {
const wallets = useWallets();
const connectedWallet = wallets.find(w => w.accounts.length > 0);
const address = connectedWallet?.accounts[0]?.address;
return (
<div style={{ maxWidth: 600, margin: '40px auto', textAlign: 'center', fontFamily: 'system-ui' }}>
<h1>Spark Mini</h1>
<p>Gas-free microblogging on IOTA testnet</p>
{!connectedWallet ? (
<>
<h2>Connect your wallet to start posting</h2>
<ConnectButton />
</>
) : (
<p>Connected: {address?.slice(0, 12)}...{address?.slice(-8)}</p>
)}
</div>
);
}
Explanation
useWallets returns all detected wallets. When a user connects, accounts.length > 0. The ConnectButton automatically opens the official modal and handles all wallet standards.
Test it: Click Connect → Choose Firefly → Success! You now see your address.
Part 3 – Fund the Gas Station Sponsor
3.1 Find the Sponsor Address
cat gas-station/config.yaml | grep -A 5 signer-config
You’ll see something like:
address: "0xc6254a6dd24bc7cc975a0890d4092c3d0c0996e8783a2e3ff338f47705405b72"
Explanation
This is the address that will pay for everyone’s transactions.
3.2 Fund It via Faucet
3.3 Verify Funding
Explanation
You should see a positive balance and many coin objects after a few seconds.
Part 4 – Implement the Sponsored Post Flow
4.1 Add Constants & Imports
// Top of src/App.tsx (add these)
import { useIotaClient, useSignTransaction } from '@iota/dapp-kit';
import { Transaction } from '@iota/iota-sdk/transactions';
import axios from 'axios';
const GAS_STATION_URL = 'http://localhost:9527';
const GAS_STATION_AUTH = 'supersecret123';
const PACKAGE_ID = '0x1111111111111111111111111111111111111111111111111111111111111111'; // Official demo package
Explanation
- P
ACKAGE_IDis the official demo media package that contains post_message(string). - The Gas Station URL and auth token match our Docker setup.
4.2 Full Post Handler (copy-paste this function)
const client = useIotaClient();
const { mutateAsync: signTransaction } = useSignTransaction();
const [content, setContent] = useState('');
const [posts, setPosts] = useState<Array<{content: string, author: string, txid: string}>>([]);
const handlePost = async () => {
if (!address || !content.trim()) return;
try {
// 1. Build transaction
const tx = new Transaction();
tx.setSender(address!);
tx.moveCall({
target: `${PACKAGE_ID}::media::post_message`,
arguments: [tx.pure.string(content)],
});
// 2. Reserve gas
const reserveRes = await axios.post(
`${GAS_STATION_URL}/v1/reserve_gas`,
{ gas_budget: 50_000_000, reserve_duration_secs: 15 },
{ headers: { Authorization: `Bearer ${GAS_STATION_AUTH}` } }
);
const { sponsor_address, reservation_id, gas_coins } = reserveRes.data.result;
// 3. Attach sponsor gas data
tx.setGasOwner(sponsor_address);
tx.setGasPayment(gas_coins);
tx.setGasBudget(50_000_000);
// 4. Build unsigned bytes
const unsignedTxBytes = await tx.build({ client });
// 5. User signs (wallet popup!)
const { signature, reportTransactionEffects } = await signTransaction({ transaction: tx });
// 6. Send to Gas Station for co-sign + execution
const txBytesBase64 = btoa(String.fromCharCode(...new Uint8Array(unsignedTxBytes)));
const executeRes = await axios.post(
`${GAS_STATION_URL}/v1/execute_tx`,
{ reservation_id, tx_bytes: txBytesBase64, user_sig: signature },
{ headers: { Authorization: `Bearer ${GAS_STATION_AUTH}` } }
);
// 7. Report success to wallet
reportTransactionEffects(executeRes.data.effects);
// 8. Update UI
setPosts(prev => [{
content,
author: address!.slice(0, 10) + '...',
txid: executeRes.data.effects.transactionDigest,
}, ...prev]);
setContent('');
alert('Spark posted on-chain!');
} catch (err: any) {
console.error(err);
alert('Failed: ' + (err.response?.data?.error || err.message));
}
};
Explanation
This is the complete sponsored transaction flow:
- User builds intent
- Reserves gas from sponsor
- Attaches sponsor’s gas objects
- Signs with wallet (Firefly popup)
- Gas Station co-signs and submits
- Success → on-chain forever!
4.3 Final UI with Textarea & Feed
Replace the return block with this beautiful final version (includes textarea, button, and feed).
return (
<div style={{ maxWidth: 600, margin: '40px auto', fontFamily: 'system-ui' }}>
<h1>Spark Mini</h1>
<p>Gas-free microblogging on IOTA testnet</p>
{!address ? (
<div style={{ textAlign: 'center', marginTop: 80 }}>
<h2>Connect your wallet to post</h2>
<ConnectButton />
</div>
) : (
<>
<p>Connected: {address.slice(0, 12)}...{address.slice(-8)}</p>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="What's your spark? (280 chars)"
maxLength={280}
rows={4}
style={{ width: '100%', padding: 12, fontSize: 16, borderRadius: 8, border: '1px solid #ccc' }}
/>
<button
onClick={handlePost}
style={{ marginTop: 12, padding: '12px 24px', background: '#0068FF', color: 'white', border: 'none', borderRadius: 8, fontSize: 16 }}
>
Post Spark
</button>
<hr style={{ margin: '40px 0' }} />
{posts.map((p, i) => (
<div key={i} style={{ padding: 16, border: '1px solid #eee', borderRadius: 12, marginBottom: 12 }}>
<strong>{p.author}:</strong> {p.content}
<br />
<small>
<a href={`https://explorer.iota.org/testnet/transaction/${p.txid}`} target="_blank">
View on Explorer
</a>
</small>
</div>
))}
</>
)}
</div>
);
Explanation
Clean, responsive UI with live feed and explorer links. Every post is permanently stored on IOTA testnet.
Final Test
npm run dev- Connect Firefly
- Type a message → Post Spark
- Approve in wallet
- Success! Your message appears + explorer link works
Resources
- Gas Station Docs: https://docs.iota.org/operator/gas-station
- dApp Kit: https://docs.iota.org/developer/ts-sdk/dapp-kit
- Discord: