# Twift AI Agent Guide

Twift is a multi-chain crypto transfer protocol with two features:
- **Multisend** — Batch token/native transfers to multiple addresses in a single transaction
- **Twist Send** — Privacy-focused transfers using a two-step deposit/release mechanism

Web app: `https://megasender.io`

This guide is written for AI agents that need to interact with Twift contracts and APIs programmatically.

---

## Step 0: Wallet Setup

Before executing any transaction, determine how signing will be handled.

### Decision Tree

```
Does the AI agent have a private key in context?
├── Yes → proceed to Step 1
└── No → ask the user:

    "To execute transactions, I need one of:
     (a) Your private key — I will sign transactions on your behalf
     (b) Manual control — I will prepare the transaction data and you sign it"
```

### Option A: AI-controlled signing (user provides private key)

> ⚠️ Never log, persist, or transmit the private key outside the session.

The user provides a raw private key (hex string, `0x`-prefixed or not). Store it only in session memory. Use it to sign and broadcast transactions via `eth_sendRawTransaction`.

Minimum viable signer setup with viem:

```typescript
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const account = privateKeyToAccount("0x<PRIVATE_KEY>");
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });
```

### Option B: User-controlled signing (AI prepares, user signs)

AI encodes and returns the unsigned transaction object:

```json
{
  "to": "0x885Ec36508e75CF605943DdB93D54fDbF58Ccfdc",
  "data": "0x<encoded_calldata>",
  "value": "0x<hex_wei>",
  "chainId": 1,
  "gas": "0x<estimated_gas>"
}
```

The user pastes this into their wallet (MetaMask → "Send" → Advanced → hex data), or uses `eth_sendTransaction` via their connected provider.

### Asset Check

Before calling any contract, verify the sending wallet has sufficient balance:

| Transfer type | Required assets |
|---------------|----------------|
| Native coin multisend (ETH, MATIC, etc.) | `sum(amounts) + gas` in native coin |
| ERC20 token multisend | `sum(amounts)` in the ERC20 token **+ gas** in native coin |
| Twist native | `total_amount + safe_gas_reserve` in native coin |
| Twist ERC20 | ERC20 tokens for transfer + native coin for gas (two transactions) |

Query balances before proceeding:

```typescript
// Native balance
const balance = await publicClient.getBalance({ address: senderAddress });

// ERC20 balance
const tokenBalance = await publicClient.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: "balanceOf",
  args: [senderAddress],
});
```

If balance is insufficient, report exact shortfall to the user and halt.

---

## Safety Rules

The agent MUST follow these rules in every transaction flow.

### Key handling
- Never log, print, write to disk, or transmit the private key outside the session.
- Never include the key in error messages, stack traces, or telemetry.
- If the user pastes a key into chat, do not echo it back.

### Before signing — show the user
Display these fields and require explicit confirmation:
- Network (chain name + chainId, with mainnet/testnet label)
- Contract address being called + function name
- For each recipient: full address (not truncated) + amount + token
- Total value being sent (sum of amounts) + estimated gas
- For ERC20: whether an `approve` transaction is also required

### Mainnet vs testnet
- If `chainId` belongs to a mainnet, prefix every confirmation message with **"⚠️ MAINNET"**.
- Never silently fall back from mainnet to testnet (or vice versa). The user must re-confirm if the chain changes.

### Failure reporting
- Always report the `txHash` even on failure.
- Never claim funds are "lost" — direct the user to the relevant block explorer or, for Twist, to https://app.safe.global with the relay Safe address.

---

## Retry & Error Handling

| Failure | Retry policy | User action |
|---------|-------------|-------------|
| RPC timeout / network error (read) | Exponential backoff, max 3 retries | Continue silently |
| `waitForTransactionReceipt` timeout (>60s) | Do NOT retry the transaction | Report `txHash` + ask user to check explorer |
| Transaction reverted (`receipt.status !== "success"`) | Do NOT retry automatically | Report decoded revert reason if available |
| `approve` succeeded, `disperseToken` reverted | Allowance is set; safe to retry `disperseToken` | — |
| History API `409 Duplicate` | Treat as success | — |
| History API `5xx` | Retry up to 3 times with backoff | If still failing, report — the on-chain tx already succeeded |
| Twist Step 1 succeeded, Step 2 failed | Funds are in the relay Safe; retry Step 2 with same Safe address | If still failing, point user to https://app.safe.global |

### Idempotency
- Never re-broadcast the same nonce. If a tx is "stuck", let the user decide whether to speed up or cancel.
- Always check whether a transaction is already in the mempool before re-submitting.

---

## Token Handling Rules

### Decimals — never assume, always read from the contract

The agent will encounter arbitrary tokens. Do NOT hardcode a decimals table — query `decimals()` on the token contract before computing amounts:

```typescript
const decimals = await publicClient.readContract({
  address: tokenAddress,
  abi: ERC20_ABI,
  functionName: "decimals",
});
const value = parseUnits(humanAmount, decimals);
```

If `decimals()` reverts or the contract does not implement it, halt and ask the user — do not guess.

### Tokens that require an allowance reset

A small number of legacy ERC20 tokens revert when changing allowance from a non-zero value to another non-zero value. For these, set allowance to `0` first, wait for confirmation, then set the new allowance:

| Token | Chain | Chain ID | Address |
|-------|-------|----------|---------|
| USDT | Ethereum | 1 | `0xdac17f958d2ee523a2206206994597c13d831ec7` |
| USDT | Ethereum Sepolia | 11155111 | `0x29B9549c33Fffd1Ba51a533Fb5d33290B470Ed5E` |

Pseudocode:

```typescript
if (allowance > 0n && allowance < required) {
  await approve(token, MULTISEND_CONTRACT, 0n);
  await waitForReceipt(...);
}
await approve(token, MULTISEND_CONTRACT, maxUint256);
```

For all other tokens, a single `approve(maxUint256)` is sufficient. If a future token reverts on a non-zero raise, treat it as a member of this list and apply the same reset pattern.

### Unknown tokens
Before sending an arbitrary token, the agent MUST:
1. Read `name()`, `symbol()`, `decimals()` from the contract.
2. Show these to the user along with the contract address.
3. Ask for explicit confirmation before proceeding.

### Fee-on-transfer / rebasing tokens
For these tokens, `sum(values)` will NOT match the amount actually delivered to recipients. The agent SHOULD warn the user and recommend an individual transfer instead of a multisend.

---

## Step 1: Contract Usage

### Supported Chains

The `Multisend` column lists every chain where the disperse contract is deployed. The `Twist Send` column marks chains where the privacy flow (ERC-4337 Safe relay) is also available — Twist Send is a strict subset of Multisend, since it reuses the same disperse contract internally but additionally requires Safe 4337 module support.

#### Mainnet

| Chain | Chain ID | Multisend Contract | Twist Send |
|-------|----------|--------------------|------------|
| Ethereum | 1 | `0x885Ec36508e75CF605943DdB93D54fDbF58Ccfdc` | ✅ |
| Base | 8453 | `0x885Ec36508e75CF605943DdB93D54fDbF58Ccfdc` | ✅ |
| Optimism | 10 | `0x885Ec36508e75CF605943DdB93D54fDbF58Ccfdc` | ✅ |
| Polygon | 137 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | ✅ |
| BNB Smart Chain | 56 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | ✅ |
| Arbitrum One | 42161 | `0x779388650638FA7e377f1EE032A9a47C95c36D95` | ✅ |
| Kaia | 8217 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | — |

#### Testnet

| Chain | Chain ID | Multisend Contract | Twist Send |
|-------|----------|--------------------|------------|
| Ethereum Sepolia | 11155111 | `0x0b50482bC09f515E8740783316bA6bcDC8dd8C43` | ✅ |
| Base Sepolia | 84532 | `0xab8a9E987fC259eC066EfC2Bd03b90eE45AC48f9` | ✅ |
| Optimism Sepolia | 11155420 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | — |
| Polygon Amoy | 80002 | `0x869F300f2dC2C1efD5D9D3a82E22B8ee010a8C69` | ✅ |
| BNB Testnet | 97 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | ✅ |
| Arbitrum Sepolia | 421614 | `0xDE75EC9f38175d75f47c3E6416d654a313fFd176` | ✅ |
| Kaia Kairos | 1001 | `0xD00AF0f40EDFA6eb6EAC5ad8556ae15cfA38a7A0` | — |

If a user requests Twist Send on a chain marked `—`, fall back to Multisend and explain that the privacy flow is not available on that network.

### Contract ABI

```json
[
  {
    "name": "disperseEther",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "recipients", "type": "address[]" },
      { "name": "values",     "type": "uint256[]" }
    ],
    "outputs": []
  },
  {
    "name": "disperseToken",
    "type": "function",
    "stateMutability": "nonpayable",
    "inputs": [
      { "name": "token",      "type": "address"   },
      { "name": "recipients", "type": "address[]" },
      { "name": "values",     "type": "uint256[]" }
    ],
    "outputs": []
  },
  {
    "name": "disperseTokenSimple",
    "type": "function",
    "stateMutability": "nonpayable",
    "inputs": [
      { "name": "token",      "type": "address"   },
      { "name": "recipients", "type": "address[]" },
      { "name": "values",     "type": "uint256[]" }
    ],
    "outputs": []
  }
]
```

Use `disperseEther` for native coin transfers and `disperseToken` for ERC20 tokens. `disperseTokenSimple` is an alternative ERC20 path for tokens that do not require an allowance reset.

### 1-A: Native Coin Multisend (`disperseEther`)

`recipients` and `values` must have identical length. `value` sent with the transaction must equal `sum(values)`.

```typescript
import { parseEther } from "viem";

const recipients = ["0xAAA...", "0xBBB...", "0xCCC..."];
const values     = [parseEther("0.1"), parseEther("0.2"), parseEther("0.05")];
const totalValue = values.reduce((a, b) => a + b, 0n);

const txHash = await walletClient.writeContract({
  address: MULTISEND_CONTRACT,
  abi: multisendAbi,
  functionName: "disperseEther",
  args: [recipients, values],
  value: totalValue,
});
```

### 1-B: ERC20 Token Multisend (`disperseToken`)

Before calling `disperseToken`, the sender must have approved the contract for at least `sum(values)`.

**Step 1 — Check and set allowance:**

```typescript
const allowance = await publicClient.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: "allowance",
  args: [senderAddress, MULTISEND_CONTRACT],
});

const totalAmount = values.reduce((a, b) => a + b, 0n);

if (allowance < totalAmount) {
  // Some tokens (e.g. USDT) require resetting to 0 before increasing
  if (allowance > 0n) {
    await walletClient.writeContract({
      address: tokenAddress,
      abi: erc20Abi,
      functionName: "approve",
      args: [MULTISEND_CONTRACT, 0n],
    });
  }
  await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "approve",
    args: [MULTISEND_CONTRACT, maxUint256],
  });
}
```

**Step 2 — Execute multisend:**

```typescript
const txHash = await walletClient.writeContract({
  address: MULTISEND_CONTRACT,
  abi: multisendAbi,
  functionName: "disperseToken",
  args: [tokenAddress, recipients, values],
});
```

### Waiting for Confirmation

Always wait for at least 1 confirmation before recording history:

```typescript
const receipt = await publicClient.waitForTransactionReceipt({
  hash: txHash,
  confirmations: 1,
});

if (receipt.status !== "success") {
  throw new Error(`Transaction reverted: ${txHash}`);
}
```

---

## Step 2: Record History via API

After a transaction is confirmed on-chain, call the Twift API to store it. This is a plain REST call — no authentication or signing required.

### Base URL

```
https://api.megasender.io
```

### POST `/api/transactions` — Record a transaction

```
POST https://api.megasender.io/api/transactions
Content-Type: application/json
```

**Request body:**

```json
{
  "chainId": 1,
  "address": "0x<sender_address>",
  "txHash": "0x<transaction_hash>",
  "metadata": {
    "type": "multisend",
    "recipientCount": 3,
    "totalAmount": "0.35",
    "tokenSymbol": "ETH",
    "recipients": [
      { "address": "0xAAA...", "amount": "0.1" },
      { "address": "0xBBB...", "amount": "0.2" },
      { "address": "0xCCC...", "amount": "0.05" }
    ],
    "status": "completed"
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `chainId` | number | ✅ | EIP-155 chain ID |
| `address` | string | ✅ | Sender wallet address |
| `txHash` | string | ✅ | Confirmed transaction hash |
| `metadata.type` | `"multisend"` \| `"twist"` | ✅ | Transfer type |
| `metadata.status` | string | — | `"completed"`, `"pending"`, `"failed"` |
| `metadata.recipientCount` | number | — | Number of recipients |
| `metadata.totalAmount` | string | — | Human-readable total (e.g. `"1.5"`) |
| `metadata.tokenSymbol` | string | — | `"ETH"`, `"USDC"`, etc. |
| `metadata.recipients` | array | — | List of `{address, amount}` pairs |

**Responses:**

| Code | Meaning |
|------|---------|
| `201` | Stored successfully |
| `400` | Invalid address, txHash format, or missing required field |
| `409` | Duplicate — same `chainId + txHash` already recorded |

### GET `/api/transactions/:address` — Fetch transaction history

```
GET https://api.megasender.io/api/transactions/0x<address>?chainId=1&page=1&limit=20
```

**Query parameters:**

| Param | Default | Description |
|-------|---------|-------------|
| `page` | `1` | Page number |
| `limit` | `50` | Items per page (max 50) |
| `chainId` | — | Filter by chain |
| `group` | — | `"mainnet"` or `"testnet"` |
| `order` | `"desc"` | `"asc"` or `"desc"` by timestamp |

**Response:**

```json
{
  "data": [
    {
      "chainId": 1,
      "address": "0x<sender>",
      "txHash": "0x<hash>",
      "group": "mainnet",
      "metadata": { "type": "multisend", "status": "completed" },
      "createdAt": "2025-04-29T10:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### GET `/api/transactions/:address/:txHash` — Fetch a specific transaction

```
GET https://api.megasender.io/api/transactions/0x<address>/0x<txHash>
```

Returns a single transaction object or `404` if not found.

---

## Address & Hash Format Rules

| Chain type | Address format | TxHash format |
|------------|---------------|---------------|
| EVM (Ethereum, Base, etc.) | `0x` + 40 hex chars, case-insensitive | `0x` + 64 hex chars |
| Solana | Base58, 32–44 chars | Base58, ~87–88 chars |

The API automatically normalizes EVM addresses and hashes to lowercase.

---

## End-to-End Flow Summary

```
1. Check wallet availability
   └─ No wallet → ask user for (a) private key or (b) manual signing

2. Check balances
   └─ Insufficient → report shortfall, halt

3. [ERC20 only] Check and set token allowance on multisend contract

4. Call contract
   ├─ Native: disperseEther(recipients, values) with value = sum(values)
   └─ ERC20:  disperseToken(token, recipients, values)

5. Wait for on-chain confirmation (receipt.status === "success")

6. POST /api/transactions to record history

7. Confirm to user with txHash and explorer link
```

**Block explorer links:**

| Chain | Explorer URL pattern |
|-------|---------------------|
| Ethereum | `https://etherscan.io/tx/<txHash>` |
| Base | `https://basescan.org/tx/<txHash>` |
| Optimism | `https://optimistic.etherscan.io/tx/<txHash>` |
| Polygon | `https://polygonscan.com/tx/<txHash>` |
| BNB Chain | `https://bscscan.com/tx/<txHash>` |
| Arbitrum | `https://arbiscan.io/tx/<txHash>` |
| Kaia | `https://kaiascan.io/tx/<txHash>` |

See [ai-agent-examples.md](./ai-agent-examples.md) for complete runnable code examples.
