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:

  1. Query ao to find a user's current delegations
  2. Let the user select what percentage to delegate to your launch
  3. Adjust down the user's current delegations if necessary
  4. 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:

  1. Limit their delegation to 30% (the available amount)
  2. 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.