Skip to main content
Attribution links your deposit and withdrawal volume to your integration — enabling fee sharing and volume reporting. If you cannot install the Gauntlet SDK, or have an existing integration that you do not want to migrate, you can add attribution manually using the ERC-8021 standard. You must request a builder code from Gauntlet — using an unregistered string appends bytes to calldata but volume will not be counted. Request your builder code during partnership onboarding.

Encode the Attribution Suffix

Attribution is an ERC-8021 calldata suffix: the marker 0x8021 followed by your builder code encoded as UTF-8 hex. Construct it once and reuse it across all transactions.
import { toHex } from 'viem'

// 16-byte ERC-8021 marker: "8021" repeated 8 times
const ERC8021_MARKER = '80218021802180218021802180218021'
const ERC8021_SCHEMA_ID = '00' // Schema 0: simple ASCII codes

function encodeAttribution(builderCode: string): `0x${string}` {
  const codeHex = toHex(builderCode).slice(2) // UTF-8 hex, no 0x prefix
  const codeByteLen = codeHex.length / 2
  const codeLengthHex = codeByteLen.toString(16).padStart(2, '0')
  return `0x${codeHex}${codeLengthHex}${ERC8021_SCHEMA_ID}${ERC8021_MARKER}`
}

const attribution = encodeAttribution('your-builder-code')
// e.g. 'acme' → '0x61636d650400' + 16-byte marker
Once the builder code is encoded, it’s ready to be included in the transaction payload. Depending on the tools used for signing, the process is different. The options are Wagmi (most common) and embedded wallets (ex. privy).

With wagmi

wagmi’s writeContract accepts a dataSuffix parameter that it appends to the ABI-encoded calldata before submitting to the wallet. Pass the attribution string there — the suffix is preserved even when the underlying EIP-1193 provider re-encodes the transaction.

Check and request token approval

import { useReadContract, useWriteContract } from 'wagmi'

const ERC20_ABI = [
  {
    type: 'function',
    name: 'allowance',
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
    ],
    outputs: [{ type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'approve',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ type: 'bool' }],
    stateMutability: 'nonpayable',
  },
] as const

const { data: allowance } = useReadContract({
  address: TOKEN_ADDRESS,
  abi: ERC20_ABI,
  functionName: 'allowance',
  args: [address, PROVISIONER_ADDRESS],
})

const { writeContractAsync } = useWriteContract()

if (allowance !== undefined && allowance < amount) {
  await writeContractAsync({
    address: TOKEN_ADDRESS,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [PROVISIONER_ADDRESS, amount],
    dataSuffix: attribution, // keep attribution consistent across all steps
  })
}
Wait for the approval to confirm before submitting the deposit — the deposit reverts if the allowance hasn’t landed yet.

Request a deposit

const PROVISIONER_ABI = [{
  type: 'function',
  name: 'requestDeposit',
  inputs: [
    { name: 'token', type: 'address' },
    { name: 'tokensIn', type: 'uint256' },
    { name: 'minUnitsOut', type: 'uint256' },
    { name: 'solverTip', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
    { name: 'maxPriceAge', type: 'uint256' },
    { name: 'isFixedPrice', type: 'bool' },
  ],
  outputs: [],
  stateMutability: 'nonpayable',
}] as const

const DAY = 86400n
const deadline = BigInt(Math.ceil(Date.now() / 1000)) + DAY * 3n

await writeContractAsync({
  address: PROVISIONER_ADDRESS,
  abi: PROVISIONER_ABI,
  functionName: 'requestDeposit',
  args: [
    TOKEN_ADDRESS,
    amount,
    0n,        // minUnitsOut — set to 0 to accept any price, or calculate slippage tolerance
    0n,        // solverTip — set to 0 unless you want to incentivize faster settlement
    deadline,  // 3 days from now
    DAY * 10n, // maxPriceAge — maximum oracle price age accepted
    false,     // isFixedPrice
  ],
  dataSuffix: attribution, // required — omitting silently drops attribution
})
Deposits are queued and settled by the Gauntlet solver (~2 hours). Check the user’s pendingDeposit balance using the API to confirm the request landed.

Request a withdrawal

For withdrawals, the spender is the provisioner and the token being approved is the vault share token (VAULT_ADDRESS), not the asset or numeraire token.
const VAULT_ABI = [{
  type: 'function',
  name: 'balanceOf',
  inputs: [{ name: 'account', type: 'address' }],
  outputs: [{ type: 'uint256' }],
  stateMutability: 'view',
}] as const

const REDEEM_ABI = [{
  type: 'function',
  name: 'requestRedeem',
  inputs: [
    { name: 'token', type: 'address' },
    { name: 'unitsIn', type: 'uint256' },
    { name: 'minTokensOut', type: 'uint256' },
    { name: 'solverTip', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
    { name: 'maxPriceAge', type: 'uint256' },
    { name: 'isFixedPrice', type: 'bool' },
  ],
  outputs: [],
  stateMutability: 'nonpayable',
}] as const

const { data: shares } = useReadContract({
  address: VAULT_ADDRESS,
  abi: VAULT_ABI,
  functionName: 'balanceOf',
  args: [address],
})

// Approve provisioner to spend vault shares if needed
const { data: vaultAllowance } = useReadContract({
  address: VAULT_ADDRESS,
  abi: ERC20_ABI,
  functionName: 'allowance',
  args: [address, PROVISIONER_ADDRESS],
})

if (vaultAllowance !== undefined && shares !== undefined && vaultAllowance < shares) {
  await writeContractAsync({
    address: VAULT_ADDRESS,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [PROVISIONER_ADDRESS, shares],
    dataSuffix: attribution,
  })
}

if (shares === undefined || shares === 0n) return

await writeContractAsync({
  address: PROVISIONER_ADDRESS,
  abi: REDEEM_ABI,
  functionName: 'requestRedeem',
  args: [
    TOKEN_ADDRESS,
    shares,    // unitsIn
    0n,           // minTokensOut — set to 0 to accept any price, or calculate slippage tolerance
    0n,           // solverTip
    deadline,
    DAY * 10n,    // maxPriceAge
    false,        // isFixedPrice
  ],
  dataSuffix: attribution, // required — omitting silently drops attribution
})

With a Privy Wallet (Embedded)

Embedded wallets expose a sendTransaction function that accepts raw transaction parameters including a data field. Encode the full calldata manually and append the attribution suffix before sending — attribution is baked into the bytes and cannot be stripped by the provider.

Check and request token approval

import { createPublicClient, encodeFunctionData, http } from 'viem'
import { base } from 'viem/chains'

const publicClient = createPublicClient({ chain: base, transport: http(RPC_URL) })

const ERC20_ABI = [
  {
    type: 'function',
    name: 'allowance',
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
    ],
    outputs: [{ type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'approve',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ type: 'bool' }],
    stateMutability: 'nonpayable',
  },
] as const

const allowance = await publicClient.readContract({
  address: TOKEN_ADDRESS,
  abi: ERC20_ABI,
  functionName: 'allowance',
  args: [userAddress, PROVISIONER_ADDRESS],
})

if (allowance < amount) {
  const approveCalldata = encodeFunctionData({
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [PROVISIONER_ADDRESS, amount],
  })

  // Append attribution to the approval so every step carries your builder code
  await sendTransaction({
    to: TOKEN_ADDRESS,
    data: `${approveCalldata}${attribution.slice(2)}`,
  })
}
Wait for the approval to confirm before submitting the deposit — the deposit reverts if the allowance hasn’t landed yet. sendTransaction here refers to Privy’s transaction function — either useSendTransaction from @privy-io/react-auth or the equivalent method on the embedded wallet object.

Request a deposit

import { encodeFunctionData } from 'viem'

const PROVISIONER_ABI = [{
  type: 'function',
  name: 'requestDeposit',
  inputs: [
    { name: 'token', type: 'address' },
    { name: 'tokensIn', type: 'uint256' },
    { name: 'minUnitsOut', type: 'uint256' },
    { name: 'solverTip', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
    { name: 'maxPriceAge', type: 'uint256' },
    { name: 'isFixedPrice', type: 'bool' },
  ],
  outputs: [],
  stateMutability: 'nonpayable',
}] as const

const DAY = 86400n
const deadline = BigInt(Math.ceil(Date.now() / 1000)) + DAY * 3n

const depositCalldata = encodeFunctionData({
  abi: PROVISIONER_ABI,
  functionName: 'requestDeposit',
  args: [
    TOKEN_ADDRESS,
    amount,
    0n,        // minUnitsOut — set to 0 to accept any price, or calculate slippage tolerance
    0n,        // solverTip — set to 0 unless you want to incentivize faster settlement
    deadline,  // 3 days from now
    DAY * 10n, // maxPriceAge — maximum oracle price age accepted
    false,     // isFixedPrice
  ],
})

// Append attribution suffix — strip '0x' before concatenating
await sendTransaction({
  to: PROVISIONER_ADDRESS,
  data: `${depositCalldata}${attribution.slice(2)}`,
})
Deposits are queued and settled by the Gauntlet solver (~2 hours). Check the user’s pendingDeposit balance using the API to confirm the request landed.

Request a withdrawal

For withdrawals, the spender is the provisioner and the token being approved is the vault share token (VAULT_ADDRESS), not the asset or numeraire token. Read the user’s share balance from the vault.
const VAULT_ABI = [{
  type: 'function',
  name: 'balanceOf',
  inputs: [{ name: 'account', type: 'address' }],
  outputs: [{ type: 'uint256' }],
  stateMutability: 'view',
}] as const

// Read the user's vault share balance
const shares = await publicClient.readContract({
  address: VAULT_ADDRESS,
  abi: VAULT_ABI,
  functionName: 'balanceOf',
  args: [userAddress],
})

// Approve the provisioner to spend vault shares if needed
const vaultAllowance = await publicClient.readContract({
  address: VAULT_ADDRESS,
  abi: ERC20_ABI,
  functionName: 'allowance',
  args: [userAddress, PROVISIONER_ADDRESS],
})

if (vaultAllowance < shares) {
  const approveCalldata = encodeFunctionData({
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [PROVISIONER_ADDRESS, shares],
  })
  await sendTransaction({
    to: VAULT_ADDRESS,
    data: `${approveCalldata}${attribution.slice(2)}`,
  })
}

const REDEEM_ABI = [{
  type: 'function',
  name: 'requestRedeem',
  inputs: [
    { name: 'token', type: 'address' },
    { name: 'unitsIn', type: 'uint256' },
    { name: 'minTokensOut', type: 'uint256' },
    { name: 'solverTip', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
    { name: 'maxPriceAge', type: 'uint256' },
    { name: 'isFixedPrice', type: 'bool' },
  ],
  outputs: [],
  stateMutability: 'nonpayable',
}] as const

const redeemCalldata = encodeFunctionData({
  abi: REDEEM_ABI,
  functionName: 'requestRedeem',
  args: [
    TOKEN_ADDRESS,
    shares,    // unitsIn — full vault share balance
    0n,        // minTokensOut — set to 0 to accept any price, or calculate slippage tolerance
    0n,        // solverTip
    deadline,  // 3 days from now
    DAY * 10n, // maxPriceAge
    false,     // isFixedPrice
  ],
})

await sendTransaction({
  to: PROVISIONER_ADDRESS,
  data: `${redeemCalldata}${attribution.slice(2)}`,
})

What’s Next

Attribution with SDK

How the SDK handles attribution automatically and how to monitor attributed volume.

Deposit Your First Dollar

SDK-based deposit and withdrawal flow using the Gauntlet SDK.