Skip to content

Bots & mobile

Currently, programmatic trading is done by calling Router contract functions directly via viem (TypeScript) or web3.py (Python). See Quickstart - TypeScript and Quickstart - Python.

Planned: API key system and webhook notifications for bot developers are planned for a future release. Currently, all trading goes through on-chain Router contract calls with standard wallet signing.


Platform Recommended
iOS Swift + WalletConnect SDK + viem-swift (TBA)
Android Kotlin + WalletConnect SDK + ethers-android
React Native wagmi/connectors + RainbowKit Mobile
Flutter walletconnect_flutter + custom contract integration
Web (custom) wagmi + viem + RainbowKit
import { WagmiConfig } from 'wagmi';
import { unichain } from 'wagmi/chains';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
const config = getDefaultConfig({
appName: 'My PrediX App',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
chains: [unichain],
});
function App() {
return (
<WagmiConfig config={config}>
<RainbowKitProvider><YourApp /></RainbowKitProvider>
</WagmiConfig>
);
}

Trade hook:

import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
// import Router ABI from source or explorer
function BuyYesButton({ marketId, usdcIn, minOut }) {
const { writeContract, data: hash } = useWriteContract();
const { isLoading } = useWaitForTransactionReceipt({ hash });
return (
<button
onClick={() => writeContract({
address: ROUTER_ADDRESS,
abi: routerAbi,
functionName: 'buyYes',
args: [marketId, usdcIn, minOut, account, 10n, deadline],
})}
disabled={isLoading}
>
{isLoading ? 'Confirming...' : 'Buy YES'}
</button>
);
}

iOS Swift:

import WalletConnectSwift
let tx = SwapTx(
to: routerAddress,
data: encodeBuyYes(marketId: marketId, usdcIn: 100, minOut: 200, ...),
value: 0,
gas: 500_000
)
session.sendTransaction(tx) { result in
switch result {
case .success(let txHash): print("Tx: \(txHash)")
case .failure(let error): print("Error: \(error)")
}
}

Android Kotlin:

import com.walletconnect.sign.client.*
val tx = mapOf(
"from" to userAddress,
"to" to routerAddress,
"data" to encodeBuyYes(...),
"value" to "0x0",
"gas" to "0x7A120"
)
SignClient.request(
Sign.Params.Request(
sessionTopic = sessionTopic,
method = "eth_sendTransaction",
params = Json.encodeToString(tx),
chainId = "eip155:130" // Unichain mainnet (beta + production)
)
) { error -> /* handle */ }
import { RainbowKitMobileProvider } from '@rainbow-me/rainbowkit-mobile';
import { unichain } from '@wagmi/core/chains';
<RainbowKitMobileProvider chains={[unichain]} walletConnectProjectId="YOUR_ID">
<App />
</RainbowKitMobileProvider>

Same hooks API as web wagmi.

import { createKernelClient } from '@zerodev/sdk';
import { createPublicClient, http } from 'viem';
import { unichain } from 'viem/chains'; // beta + production on Unichain mainnet
const publicClient = createPublicClient({
chain: unichain,
transport: http('https://mainnet.unichain.org'),
});
const kernelClient = createKernelClient({
publicClient,
bundlerTransport: http(`${BACKEND_BASE_URL}/api/aa/bundler`),
paymasterTransport: http(`${BACKEND_BASE_URL}/api/aa/paymaster/sponsor`),
validator: passkeyValidator,
});
const userOpHash = await kernelClient.sendUserOperation({
callData: await kernelClient.encodeCalls([
{ to: USDC, data: approveCalldata },
{ to: ROUTER, data: buyYesCalldata },
]),
});

iOS - ASAuthorizationPlatformPublicKeyCredentialProvider:

import AuthenticationServices
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "predix.app"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challengeData, name: "user@predix.app", userID: userIdData
)
ASAuthorizationController(authorizationRequests: [request]).performRequests()

Android - Credential Manager API:

import androidx.credentials.*
val request = CreatePublicKeyCredentialRequest(requestJson = passkeyRequestJson)
credentialManager.createCredentialAsync(request, activity, null, executor, callback)

Smart account address is derived from the passkey public key - same address across devices if cloud-synced.

  • Caching: wagmi auto-caches with a 60s default. User-specific data (portfolio): 30s. SWR / React Query revalidate-on-focus pattern.

  • RPC efficiency: Multicall3 (0xcA11bde05977b3631167028862bE2a173976CA11) batch reads. WebSocket instead of polling. Indexer API instead of RPC getLogs for historical data.

  • Gas estimation: add 20% buffer.

    const estimatedGas = await publicClient.estimateContractGas({...});
    const gasLimit = (estimatedGas * 120n) / 100n;
Error Cause Fix
User rejected request User cancelled in wallet Show retry UI
Insufficient funds for gas EOA lacks ETH, or smart account has no balance + tx not sponsored Bridge ETH (EOA), or check sponsor eligibility (both account types)
Internal JSON-RPC error RPC down or rate limited Fallback RPC, retry
Network mismatch Wallet not connected to Unichain Auto switch via wallet_switchEthereumChain
Nonce too low Tx race condition Refresh nonce, retry
// Unichain mainnet (beta + production)
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x82', // 130 hex
chainName: 'Unichain',
rpcUrls: ['https://mainnet.unichain.org'],
blockExplorerUrls: ['https://uniscan.xyz'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
}],
});