We recently went live with the Load Network fair launch and built a custom delegation page to make it easy to delegate AO yield for $LOAD. Doing this, we learned how to interface with ao's fair launch processes to control delegation outside of the official dashboard.
This tutorial collects what we learned and should help you to build a delegation interface for your own fair launch.
Roughly we'll:
- Query ao to find a user's current delegations
- Let the user select what percentage to delegate to your launch
- Adjust down the user's current delegations if necessary
- Send the new delegation percentages to ao
1. Connecting to ao
We'll use aoconnect to communicate with ao, and on the UI we use the simple window.arweaveWallet
interface documented here and injected by wallets like Wander.
import { connect, createDataItemSigner } from "@permaweb/aoconnect";
const { result, results, message, spawn, monitor, unmonitor, dryrun } = connect({
MU_URL: "https://mu.ao-testnet.xyz",
CU_URL: "https://cu.ao-testnet.xyz",
GATEWAY_URL: "https://arweave.net",
});
2. Essential consts
Get these process IDs from your specific ao fair launch setup. The delegation controller manages all delegations, and your launch process ID is where users delegate to, found on the delegations page. This is not the same as your token process ID.
const D_CONTROLLER = 'cuxSKjGJ-WDB9PzSkVkVVrIBSh3DrYHYz44usQOj5yE'; // The global delegation controller process
const YOUR_LAUNCH_PROCESS = 'Qz3n2P-EiWNoWsvk7gKLtrV9ChvSXQ5HJPgPklWEgQ0'; // your AO-Fair-Launch-Process as shown in the list at ao.arweave.dev
3. Querying current delegations
All delegations must total 100%. If a user wants to delegate 60% to your token but already has 70% delegated elsewhere, they need to either:
- Limit their delegation to 30% (the available amount)
- Reduce other delegations to make room
const getDelegations = async (walletAddress) => {
try {
const result = await dryrun({
process: D_CONTROLLER,
data: walletAddress,
tags: [
{ name: 'Action', value: 'Get-Delegations' },
{ name: 'Wallet', value: walletAddress },
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'Type', value: 'Message' },
{ name: 'Variant', value: 'ao.TN.1' }
],
anchor: '00000000000000000000000000007046',
});
if (result?.Messages?.length > 0) {
const delegationData = JSON.parse(result.Messages[0].Data);
return delegationData.delegationPrefs || [];
}
return [];
} catch (error) {
console.error("Error fetching delegations:", error);
return [];
}
};
Understanding the response
The delegation data contains an array of preferences:
// Example response structure:
{
delegationPrefs: [
{ walletTo: "process-id-1", factor: 5000 }, // 50% (5000 basis points)
{ walletTo: "process-id-2", factor: 3000 },
]
}
note: factor
is in basis points where 10000 = 100%, 5000 = 50%, etc.
4. The delegation balance problem
We had to grab the user's current delegations in step 3 so we can show how many percent remains open, or calculate how much to squeeze other delegations down by.
const calculateDelegationLimits = (currentDelegations, yourTokenProcess) => {
let totalOtherDelegations = 0;
let currentYourTokenDelegation = 0;
currentDelegations.forEach(pref => {
if (pref.walletTo === yourTokenProcess) {
currentYourTokenDelegation = pref.factor / 100; // Convert to percentage
} else {
totalOtherDelegations += pref.factor / 100;
}
});
// Maximum possible delegation without affecting others
const maxPossibleDelegation = 100 - totalOtherDelegations;
return {
currentYourTokenDelegation,
totalOtherDelegations,
maxPossibleDelegation
};
};
5. Setting delegations
Here's where we call ao with a signed message to set delegation percentages.
const setDelegation = async (walletAddress, percentage, adjustOthers = false) => {
const signer = createDataItemSigner(window.arweaveWallet);
const factor = percentage * 100; // Convert percentage to basis points
try {
// If user wants to exceed available space and chose to adjust others
if (adjustOthers) {
await adjustOtherDelegations(walletAddress, percentage);
}
// Set the main delegation
const data = JSON.stringify({
walletFrom: walletAddress,
walletTo: YOUR_LAUNCH_PROCESS,
factor: factor
});
const messageId = await message({
process: D_CONTROLLER,
signer: signer,
data: data,
tags: [
{ name: 'Action', value: 'Set-Delegation' },
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'Type', value: 'Message' },
{ name: 'Variant', value: 'ao.TN.1' }
],
anchor: '00000000000000000000000000007046',
});
return messageId;
} catch (error) {
console.error("Error setting delegation:", error);
throw error;
}
};
6. Adjusting Other Delegations
Warn users when they need to adjust other delegations to accomodate yours! Display the impact of their choices (e.g., "This will reduce your other delegations by 20%").
const adjustOtherDelegations = async (walletAddress, desiredPercentage) => {
const currentDelegations = await getDelegations(walletAddress);
const signer = createDataItemSigner(window.arweaveWallet);
// If user wants 100%, zero out all others
if (desiredPercentage === 100) {
for (const pref of currentDelegations) {
if (pref.walletTo !== YOUR_LAUNCH_PROCESS) {
await message({
process: D_CONTROLLER,
signer: signer,
data: JSON.stringify({
walletFrom: walletAddress,
walletTo: pref.walletTo,
factor: 0
}),
tags: [
{ name: 'Action', value: 'Set-Delegation' },
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'Type', value: 'Message' },
{ name: 'Variant', value: 'ao.TN.1' }
],
anchor: '00000000000000000000000000007046',
});
}
}
} else {
// Proportionally reduce other delegations
const otherDelegationsTotal = currentDelegations
.filter(pref => pref.walletTo !== YOUR_LAUNCH_PROCESS)
.reduce((sum, pref) => sum + pref.factor, 0);
const availableSpace = 10000 - (desiredPercentage * 100);
const reductionFactor = availableSpace / otherDelegationsTotal;
for (const pref of currentDelegations) {
if (pref.walletTo !== YOUR_LAUNCH_PROCESS) {
const newFactor = Math.floor(pref.factor * reductionFactor);
await message({
process: D_CONTROLLER,
signer: signer,
data: JSON.stringify({
walletFrom: walletAddress,
walletTo: pref.walletTo,
factor: newFactor
}),
tags: [
{ name: 'Action', value: 'Set-Delegation' },
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'Type', value: 'Message' },
{ name: 'Variant', value: 'ao.TN.1' }
],
anchor: '00000000000000000000000000007046',
});
}
}
}
};
7. Getting metadata for a user's other delegations
It's helpful to pull in the full metadata of a user's other delegations so they can make an informed choice about what the thing they're about to click actually does. We don't want to (accidentally or deliberately) build a system which just wipes a finely-tuned delegation setup.
const getProcessInfo = async (processId) => {
try {
const result = await dryrun({
Owner: "123456789",
Target: processId,
Tags: [{ name: 'Action', value: 'Info' }]
});
if (result?.Messages?.length > 0) {
const tags = result.Messages[0].Tags;
const nameTag = tags.find(tag => tag.name === "Token-Name");
return nameTag?.value || `${processId.substring(0, 6)}...`;
}
} catch (error) {
console.error("Error fetching process info:", error);
}
return `${processId.substring(0, 6)}...`;
};
This foundation should give you everything needed to build a robust delegation interface for your AO token fair launch. Check out the Load Network delegations page for a live example.