# Gift Vault (FlapXVault)

## Overview

The Gift Vault (also known as FlapXVault) allows you to assign an X (Twitter) account as the **Gift Owner**, who can direct the trading fees to any EVM address of their choice:

1. **Assign Gift Owner**: The X account you specify becomes the gift owner who controls where fees go.
2. **Flexible Beneficiary**: The gift owner can assign fees to any EVM address and change it anytime.
3. **7-Day Grace Period**: If the gift owner doesn't manage the fees at least once in the first 7 days, control is forfeited.
4. **Fallback: Buyback & Burn**: If control is forfeited, fees will automatically be used for buyback and burn.

You can use it as a way to give tax fee to a X user or you can build more interesting use cases on top of it! We build the gift vault to make it easier for a web2 developer or an agent to manage the tax fee through social proof. Here are some interesting ideas:

* Create an X account for an agent and launch a tax token that assigns the agent as the gift owner. The agent can then route the tax fee to any EVM address. The Agent can manage his own fund by accepting the funding request through X: The requesters can tweet to the agent's X account and ask the agent to support them by routing the tax fee to their address for some duration. The agent can choose to accept or reject the request by managing the vault through tweeting. This could be a charity funds or a VC that supports new projects.
* A social game where players tweet to a game X account, and gets the tax fee routed to their address based on some social tasks or achievements.

In the following sections, we introduce how to integrate with the Gift Vault through its API. By following this guide, you will be able to build a complete integration that allows users to manage their vaults using X tweets as social proof.

## Table of contents

1. [Prerequisites](#prerequisites)
2. [Network Details](#network-details)
3. [Architecture overview](#architecture-overview)
4. [Step-by-step integration](#step-by-step-integration)
5. [Complete code example](#complete-code-example)
6. [API reference](#api-reference)
7. [Error handling](#error-handling)

## Prerequisites

* **Web3 Library**: ethers.js v6, viem, or web3.js
* **Vault Factory Contract Address**: See [Network Details](#network-details)
* **Tax Token Address**
* **X Handle** (Twitter username of vault owner)
* **Tweet** (you must fetch tweet data yourself)

## Network Details

### BNB Smart Chain Mainnet

* **Gift Vault Factory**: `0x025549F52B03cF36f9e1a337c02d3AA7Af66ab32`
* **Relayer API Endpoint**: `https://bnb-x-relayer.taxed.fun/submit`

### BNB Smart Chain Testnet

* **Gift Vault Factory**: `0xa02DA44D67DB6D692efa7f751b5952bd670d5326`
* **Relayer API Endpoint**: `https://bnbtest-x-relayer.taxed.fun/submit`

## Architecture overview

Flow Diagram:

```
┌──────────────────┐
│  Start Request   │
└────────┬─────────┘
         │
         ▼
┌─────────────────────────────────────┐
│ Step 1: Parse & Validate Tweet     │
│ - Fetch tweet from Twitter API      │
│ - Verify author matches xHandle     │
│ - Parse tweet text format           │
│ - Verify tax token matches          │
└────────┬────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│ ⚠️  Step 2: MANDATORY PREFLIGHT     │
│ Call canManageVault() on-chain      │
│                                     │
│ ⚠️  Backend is rate-limited (1/min) │
│ ⚠️  NEVER skip this step!           │
└────────┬────────────────────────────┘
         │
         ▼
    ┌────────┐
    │ Pass?  │
    └───┬─┬──┘
  Yes   │ │   No → STOP! Do NOT call backend
        │ 
        ▼
┌─────────────────────────────────────┐
│ Step 3: Submit to Backend (1/min)  │
│ POST /submit                        │
│ {tax_token, tweet_id}              │
└────────┬────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│ Step 4: Poll canManageVault()      │
│ Every 15s for 60s max               │
│ Success = errorMessage:"outdated"   │
└─────────────────────────────────────┘
```

## Step-by-step integration

### Step 1: Parse and validate tweet content

Integrators must fetch the tweet themselves. The tweet format is **strictly enforced**:

**Tweet Format:** `gift the fee from [tax_token_address] to [target_address] #FlapGift`

**Implementation:**

```typescript
// Type definitions
interface ParsedTweet {
  targetAddress: string;
  taxTokenAddress: string;
}

interface Tweet {
  __typename: string;
  text?: string;
  user?: {
    screen_name?: string;
    legacy?: { screen_name?: string };
  };
}

// Parse tweet text using regex
function parseTweetText(text: string): ParsedTweet | null {
  // Pattern: "gift the fee from <tax_token> to <target> #FlapGift"
  const regex = /gift\s+the\s+fee\s+from\s+(0x[a-fA-F0-9]{40})\s+to\s+(0x[a-fA-F0-9]{40})\s+#FlapGift/i;
  const match = text.match(regex);
  
  if (!match) {
    return null;
  }
  
  return {
    taxTokenAddress: match[1],  // First captured group
    targetAddress: match[2]     // Second captured group
  };
}

// Validate tweet content
function validateTweetContent(
  tweet: Tweet,
  expectedXHandle: string,
  expectedTaxToken: string
): ParsedTweet {
  
  // Check tweet exists and is valid
  if (!tweet || tweet.__typename !== "Tweet") {
    throw new Error("Tweet is not valid (deleted, suspended, or not found)");
  }
  
  // Check tweet has text
  if (!tweet.text) {
    throw new Error("Tweet has no text content");
  }
  
  // Check tweet has user data
  if (!tweet.user) {
    throw new Error("Tweet has no user data");
  }
  
  // Verify tweet author matches expected xHandle (case-insensitive)
  const tweetScreenName = tweet.user.screen_name || tweet.user.legacy?.screen_name;
  if (!tweetScreenName) {
    throw new Error("Tweet author screen_name not found");
  }
  
  if (tweetScreenName.toLowerCase() !== expectedXHandle.toLowerCase()) {
    throw new Error(
      `Tweet author mismatch. Expected: @${expectedXHandle}, Found: @${tweetScreenName}`
    );
  }
  
  // Parse tweet text format
  const parsed = parseTweetText(tweet.text);
  
  if (!parsed) {
    throw new Error(
      "Tweet text does not match the required format. " +
      "Expected: 'gift the fee from [tax token address] to [EVM address] #FlapGift'"
    );
  }
  
  // Verify tax token matches
  if (parsed.taxTokenAddress.toLowerCase() !== expectedTaxToken.toLowerCase()) {
    throw new Error(
      `Tax token mismatch. Expected: ${expectedTaxToken}, Found: ${parsed.taxTokenAddress}`
    );
  }
  
  return parsed;
}
```

### Step 2: ⚠️ MANDATORY PREFLIGHT CHECK

**ALWAYS call this before backend submission!**

**Using ethers.js v6:**

```typescript
import { Contract, JsonRpcProvider } from 'ethers';

// Minimal ABI for canManageVault
const VAULT_FACTORY_ABI = [
  {
    "inputs": [
      {"internalType": "address", "name": "taxToken", "type": "address"},
      {"internalType": "string", "name": "xHandle", "type": "string"},
      {"internalType": "uint128", "name": "tweetId", "type": "uint128"}
    ],
    "name": "canManageVault",
    "outputs": [
      {"internalType": "bool", "name": "canManage", "type": "bool"},
      {"internalType": "string", "name": "errorMessage", "type": "string"}
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

async function preflightCheck(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<void> {
  console.log("⚠️  PREFLIGHT CHECK (Mandatory before backend call)");
  console.log("   Prevents rate limit violations (1 req/min)");
  
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);
  
  // xHandle MUST be lowercase for contract compatibility
  const result = await contract.canManageVault(
    taxToken,
    xHandle.toLowerCase(),
    BigInt(tweetId)
  );
  
  const canManage: boolean = result[0];
  const errorMessage: string = result[1];
  
  if (!canManage) {
    throw new Error(
      `❌ PREFLIGHT CHECK FAILED: ${errorMessage}\n` +
      `   DO NOT call backend API - request will be rejected!`
    );
  }
  
  console.log("✅ Preflight check PASSED - safe to proceed");
}
```

**Using viem:**

```typescript
import { createPublicClient, http } from 'viem';

const VAULT_FACTORY_ABI = [
  {
    inputs: [
      { name: 'taxToken', type: 'address' },
      { name: 'xHandle', type: 'string' },
      { name: 'tweetId', type: 'uint128' }
    ],
    name: 'canManageVault',
    outputs: [
      { name: 'canManage', type: 'bool' },
      { name: 'errorMessage', type: 'string' }
    ],
    stateMutability: 'view',
    type: 'function'
  }
] as const;

async function preflightCheckViem(
  rpcUrl: string,
  factoryAddress: `0x${string}`,
  taxToken: `0x${string}`,
  xHandle: string,
  tweetId: bigint
): Promise<void> {
  
  const client = createPublicClient({
    transport: http(rpcUrl)
  });
  
  const result = await client.readContract({
    address: factoryAddress,
    abi: VAULT_FACTORY_ABI,
    functionName: 'canManageVault',
    args: [
      taxToken,
      xHandle.toLowerCase(), // MUST be lowercase
      tweetId
    ]
  });
  
  const [canManage, errorMessage] = result;
  
  if (!canManage) {
    throw new Error(`Preflight failed: ${errorMessage}`);
  }
}
```

{% hint style="warning" %}
**Important Notes:**

* **xHandle must be lowercase** when calling the contract
* This is a **view function** - no gas required, instant response
* Returns `[bool canManage, string errorMessage]`
* **ALWAYS check this before backend** to avoid rate limit violations
  {% endhint %}

### Step 3: Submit to backend API

**⚠️ ONLY call if Step 2 passed!**

**Rate Limits:**

* **1 request per minute per IP**
* **Violating may result in IP ban**

**Endpoint:** `POST /submit`

```typescript
interface SubmitRequest {
  tax_token: string;  // Ethereum address (0x...)
  tweet_id: string;   // Twitter/X tweet ID as string
}

interface SubmitResponse {
  message: string;
  x_proof: {
    target_address: string;
    tax_token: string;
    x_handle: string;
    x_id: string;
    tweet_id: string;
  };
  signature: string;  // Oracle signature (hex)
}

async function submitToBackend(
  relayerEndpoint: string,
  taxToken: string,
  tweetId: string
): Promise<SubmitResponse> {
  
  const url = `${relayerEndpoint}submit`;
  
  console.log("📡 Submitting to backend (rate-limited: 1/min)");
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      tax_token: taxToken,
      tweet_id: tweetId,
    })
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(`Backend error: ${errorData.error}`);
  }

  const data: SubmitResponse = await response.json();
  console.log("✅ Backend submission successful");
  
  return data;
}
```

### Step 4: Poll for on-chain confirmation

The relayer processes transactions asynchronously. Poll `canManageVault` to confirm success.

**Why Poll?**

* Relayer doesn't return transaction hash
* Transaction is queued for later processing
* Once processed, `canManageVault` returns `false` with error: "The tweet is outdated"

**Implementation:**

```typescript
async function pollForConfirmation(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<boolean> {
  
  const startTime = Date.now();
  const TIMEOUT = 60000; // 60 seconds
  const POLL_INTERVAL = 15000; // 15 seconds
  
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);
  
  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      const elapsed = Date.now() - startTime;
      
      // Timeout after 60 seconds
      if (elapsed >= TIMEOUT) {
        clearInterval(interval);
        reject(new Error("Polling timeout (60s) - transaction may still be processing"));
        return;
      }
      
      try {
        const result = await contract.canManageVault(
          taxToken,
          xHandle.toLowerCase(),
          BigInt(tweetId)
        );
        
        const canManage: boolean = result[0];
        const errorMessage: string = result[1];
        
        // Success: tweet marked as "outdated" means proof was recorded
        if (!canManage && errorMessage === "The tweet is outdated") {
          clearInterval(interval);
          console.log("✅ Transaction confirmed on-chain!");
          resolve(true);
          return;
        }
        
        console.log(`  Polling... (${Math.floor(elapsed / 1000)}s elapsed)`);
        
      } catch (error) {
        console.error("Polling error:", error);
      }
    }, POLL_INTERVAL);
  });
}
```

## Complete code example

```typescript
import { Contract, JsonRpcProvider } from 'ethers';

// Configuration
const FACTORY_ADDRESS = "0xa02DA44D67DB6D692efa7f751b5952bd670d5326";
const RELAYER_ENDPOINT = "https://xrelayer.taxed.fun/";
const RPC_URL = "https://your-rpc-endpoint";

const VAULT_FACTORY_ABI = [
  {
    "inputs": [
      {"name": "taxToken", "type": "address"},
      {"name": "xHandle", "type": "string"},
      {"name": "tweetId", "type": "uint128"}
    ],
    "name": "canManageVault",
    "outputs": [
      {"name": "canManage", "type": "bool"},
      {"name": "errorMessage", "type": "string"}
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

// Type definitions
interface ParsedTweet {
  targetAddress: string;
  taxTokenAddress: string;
}

interface Tweet {
  __typename: string;
  text?: string;
  user?: {
    screen_name?: string;
    legacy?: { screen_name?: string };
  };
}

interface SubmitResponse {
  message: string;
  x_proof: {
    target_address: string;
    tax_token: string;
    x_handle: string;
    x_id: string;
    tweet_id: string;
  };
  signature: string;
}

// Parse tweet text using regex
function parseTweetText(text: string): ParsedTweet | null {
  const regex = /gift\s+the\s+fee\s+from\s+(0x[a-fA-F0-9]{40})\s+to\s+(0x[a-fA-F0-9]{40})\s+#FlapGift/i;
  const match = text.match(regex);
  
  if (!match) {
    return null;
  }
  
  return {
    taxTokenAddress: match[1],
    targetAddress: match[2]
  };
}

// Validate tweet content
function validateTweetContent(
  tweet: Tweet,
  expectedXHandle: string,
  expectedTaxToken: string
): ParsedTweet {
  
  if (!tweet || tweet.__typename !== "Tweet") {
    throw new Error("Tweet is not valid (deleted, suspended, or not found)");
  }
  
  if (!tweet.text) {
    throw new Error("Tweet has no text content");
  }
  
  if (!tweet.user) {
    throw new Error("Tweet has no user data");
  }
  
  const tweetScreenName = tweet.user.screen_name || tweet.user.legacy?.screen_name;
  if (!tweetScreenName) {
    throw new Error("Tweet author screen_name not found");
  }
  
  if (tweetScreenName.toLowerCase() !== expectedXHandle.toLowerCase()) {
    throw new Error(
      `Tweet author mismatch. Expected: @${expectedXHandle}, Found: @${tweetScreenName}`
    );
  }
  
  const parsed = parseTweetText(tweet.text);
  
  if (!parsed) {
    throw new Error(
      "Tweet text does not match the required format. " +
      "Expected: 'gift the fee from [tax token address] to [EVM address] #FlapGift'"
    );
  }
  
  if (parsed.taxTokenAddress.toLowerCase() !== expectedTaxToken.toLowerCase()) {
    throw new Error(
      `Tax token mismatch. Expected: ${expectedTaxToken}, Found: ${parsed.taxTokenAddress}`
    );
  }
  
  return parsed;
}

// Preflight check
async function preflightCheck(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<void> {
  console.log("⚠️  PREFLIGHT CHECK (Mandatory before backend call)");
  
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);
  
  const result = await contract.canManageVault(
    taxToken,
    xHandle.toLowerCase(),
    BigInt(tweetId)
  );
  
  const canManage: boolean = result[0];
  const errorMessage: string = result[1];
  
  if (!canManage) {
    throw new Error(
      `❌ PREFLIGHT CHECK FAILED: ${errorMessage}\n` +
      `   DO NOT call backend API - request will be rejected!`
    );
  }
  
  console.log("✅ Preflight check PASSED");
}

// Submit to backend
async function submitToBackend(
  relayerEndpoint: string,
  taxToken: string,
  tweetId: string
): Promise<SubmitResponse> {
  
  const url = `${relayerEndpoint}submit`;
  
  console.log("📡 Submitting to backend (rate-limited: 1/min)");
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      tax_token: taxToken,
      tweet_id: tweetId
    })
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(`Backend error: ${errorData.error}`);
  }

  const data: SubmitResponse = await response.json();
  console.log("✅ Backend submission successful");
  
  return data;
}

// Poll for confirmation
async function pollForConfirmation(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<boolean> {
  
  const startTime = Date.now();
  const TIMEOUT = 60000;
  const POLL_INTERVAL = 15000;
  
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);
  
  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      const elapsed = Date.now() - startTime;
      
      if (elapsed >= TIMEOUT) {
        clearInterval(interval);
        reject(new Error("Polling timeout (60s)"));
        return;
      }
      
      try {
        const result = await contract.canManageVault(
          taxToken,
          xHandle.toLowerCase(),
          BigInt(tweetId)
        );
        
        const canManage: boolean = result[0];
        const errorMessage: string = result[1];
        
        if (!canManage && errorMessage === "The tweet is outdated") {
          clearInterval(interval);
          console.log("✅ Transaction confirmed on-chain!");
          resolve(true);
          return;
        }
        
        console.log(`  Polling... (${Math.floor(elapsed / 1000)}s elapsed)`);
        
      } catch (error) {
        console.error("Polling error:", error);
      }
    }, POLL_INTERVAL);
  });
}

// Main integration function
async function submitVaultProof(
  tweet: Tweet,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<SubmitResponse> {
  const provider = new JsonRpcProvider(RPC_URL);
  
  try {
    console.log("=== Gift Vault Proof Submission ===\n");
    
    // STEP 1: Validate Tweet Content
    console.log("Step 1: Validating tweet content...");
    const parsed = validateTweetContent(tweet, xHandle, taxToken);
    console.log("✅ Tweet validation passed");
    console.log(`   Author: @${tweet.user?.screen_name}`);
    console.log(`   Tax Token: ${parsed.taxTokenAddress}`);
    console.log(`   Target: ${parsed.targetAddress}\n`);
    
    // STEP 2: MANDATORY PREFLIGHT CHECK
    console.log("Step 2: ⚠️  PREFLIGHT CHECK");
    await preflightCheck(provider, FACTORY_ADDRESS, taxToken, xHandle, tweetId);
    console.log("✅ Preflight passed - safe to call backend\n");
    
    // STEP 3: Submit to Backend
    console.log("Step 3: Submitting to backend...");
    const proofData = await submitToBackend(RELAYER_ENDPOINT, taxToken, tweetId);
    console.log("✅ Backend accepted submission\n");
    
    // STEP 4: Poll for Confirmation
    console.log("Step 4: Waiting for on-chain confirmation...");
    await pollForConfirmation(provider, FACTORY_ADDRESS, taxToken, xHandle, tweetId);
    
    console.log("\n✅ SUCCESS: Vault proof recorded on-chain!");
    console.log("=== Submission Complete ===");
    
    return proofData;
    
  } catch (error) {
    console.error("\n❌ FAILED:", error);
    throw error;
  }
}

// Usage Example
async function main() {
  const tweet = {
    __typename: "Tweet",
    text: "gift the fee from 0x1234567890abcdef1234567890abcdef12345678 to 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd #FlapGift",
    user: {
      screen_name: "username"
    }
  };
  
  await submitVaultProof(
    tweet,
    "0x1234567890abcdef1234567890abcdef12345678",
    "username",
    "1234567890123456789"
  );
}
```

## API reference

### Tweet content validation

#### parseTweetText(text: string)

**Regex Pattern:**

```
/gift\s+the\s+fee\s+from\s+(0x[a-fA-F0-9]{40})\s+to\s+(0x[a-fA-F0-9]{40})\s+#FlapGift/i
```

**Valid Example:**

```
gift the fee from 0x1234567890abcdef1234567890abcdef12345678 to 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd #FlapGift
```

**Invalid Examples:**

```
send fees from 0x... to 0x...           // Wrong keyword
gift fees to 0x... from 0x...           // Wrong order
gift the fee from 0x123 to 0xabc        // Invalid address format
```

### Contract: canManageVault()

**Parameters:**

* `taxToken` (address): Tax token contract address
* `xHandle` (string): X handle (MUST be lowercase)
* `tweetId` (uint128): Tweet ID

**Returns:**

* `canManage` (bool): Whether vault can be managed
* `errorMessage` (string): Error description if false

**Possible Error Messages:**

| Error Message                             | Meaning                       |
| ----------------------------------------- | ----------------------------- |
| "FlapXVault not found for this tax token" | No vault exists               |
| "xHandle does not match"                  | Wrong X handle                |
| "Vault is in snowball mode"               | Vault cannot be managed       |
| "The tweet is outdated"                   | Tweet already used (success!) |
| "" (empty string)                         | Can manage (success)          |

### Backend API: POST /submit

**Endpoint:** `{relayerEndpoint}/submit`\
**Rate Limit:** 1 request per minute per IP

**Request:**

```json
{
  "tax_token": "0x1234567890abcdef...",
  "tweet_id": "1234567890123456789"
}
```

**Success Response (200):**

```json
{
  "message": "Proof submitted successfully",
  "x_proof": {
    "target_address": "0xabcdef...",
    "tax_token": "0x123456...",
    "x_handle": "username",
    "x_id": "123456789",
    "tweet_id": "1234567890123456789"
  },
  "signature": "0x1234abcd..."
}
```

**Error Response (4xx/5xx):**

```json
{
  "error": "Error description"
}
```

## Error handling

### Common errors

| Error                       | Cause             | Solution                |
| --------------------------- | ----------------- | ----------------------- |
| **Tweet Validation**        |                   |                         |
| "Tweet is not valid"        | Deleted/suspended | Use valid, active tweet |
| "Tweet has no text"         | Media-only tweet  | Ensure text content     |
| "Author mismatch"           | Wrong X handle    | Verify correct user     |
| "Format mismatch"           | Incorrect format  | Use exact format        |
| "Token mismatch"            | Wrong address     | Verify addresses match  |
| **Preflight Check**         |                   |                         |
| "Vault not found"           | No vault exists   | Check token has vault   |
| "xHandle does not match"    | Wrong vault owner | Use correct X handle    |
| "Vault is in snowball mode" | Vault locked      | Cannot manage anymore   |
| "Tweet is outdated"         | Already used      | Use new tweet ID        |
| **Backend**                 |                   |                         |
| Rate limit error            | Too many requests | Wait 60 seconds         |
| Network timeout             | Backend slow      | Retry later             |

## Manual submission fallback

If the backend relayer times out after 60 seconds, you can submit the proof directly on-chain using the proof data returned from the backend:

```typescript
async function manualSubmit(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  proofData: SubmitResponse,
  signer: any  // Wallet signer
): Promise<void> {
  
  const MANUAL_SUBMIT_ABI = [
    {
      "inputs": [
        {"name": "taxToken", "type": "address"},
        {
          "name": "proof",
          "type": "tuple",
          "components": [
            {"name": "targetAddress", "type": "address"},
            {"name": "taxToken", "type": "address"},
            {"name": "xHandle", "type": "string"},
            {"name": "XId", "type": "uint256"},
            {"name": "tweetId", "type": "uint128"}
          ]
        },
        {"name": "signature", "type": "bytes"}
      ],
      "name": "manageByProof",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ];
  
  const contract = new Contract(factoryAddress, MANUAL_SUBMIT_ABI, signer);
  
  const tx = await contract.manageByProof(
    taxToken,
    {
      targetAddress: proofData.x_proof.target_address,
      taxToken: proofData.x_proof.tax_token,
      xHandle: proofData.x_proof.x_handle,
      XId: BigInt(proofData.x_proof.x_id),
      tweetId: BigInt(proofData.x_proof.tweet_id)
    },
    proofData.signature
  );
  
  console.log("Manual submission tx:", tx.hash);
  await tx.wait();
  console.log("✅ Manual submission confirmed");
}
```

## Summary checklist

1. ✅ Fetch tweet (you must do this yourself)
2. ✅ Validate tweet content (Step 1)
   * Check tweet exists and is valid
   * Verify author matches X handle
   * Parse tweet text using regex
   * Verify tax token matches
3. ✅ ⚠️ MANDATORY: Call canManageVault() (Step 2)
   * xHandle MUST be lowercase
   * If false, STOP - do NOT call backend
4. ✅ Submit to backend (Step 3)
   * Only if preflight passed
   * Rate limited: 1 request/minute
5. ✅ Poll canManageVault() (Step 4)
   * Every 15s for max 60s
   * Success = error: "The tweet is outdated"
6. ✅ Error handling
   * Implement timeouts
   * Have manual submission fallback

{% hint style="danger" %}
**Key Points:**

* NEVER skip preflight check - prevents IP ban
* Backend is rate-limited (1/min) - no exceptions
* xHandle must be lowercase for contract calls
* Polling confirms success - relayer is async
* Consider showing manual submission option to users after 30 seconds of polling, while continuing to poll up to 60 seconds in the background
  {% endhint %}

## Gift Vault interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/// @title IFlapXVault
/// @notice Interface for the Gift Vault (FlapXVault) implementation.
/// @dev Derived from the on-chain FlapXVault contract.
interface IFlapXVault {
	/// @notice The vault states.
	enum State {
		ACCUMULATING,
		STREAMING,
		FALLBACK_SNOWBALL
	}

	/// @notice The type of balance update for snowball events.
	enum BalanceUpdateType {
		ACCUMULATION,
		SNOWBALL
	}

	/// @notice Aggregate vault statistics.
	/// @param totalBNBSpent Total BNB spent on buyback.
	/// @param totalTokenBurn Total tax tokens burned.
	struct VaultStats {
		uint128 totalBNBSpent;
		uint128 totalTokenBurn;
	}

	/// @notice Historical proof record.
	/// @param XId The X (Twitter) account id.
	/// @param tweetId The tweet id used as proof.
	/// @param targetAddress The streaming target address.
	struct ProofRecord {
		uint128 XId;
		uint128 tweetId;
		address targetAddress;
	}

	/// @notice XProof structure for EIP-712 signature verification.
	/// @param targetAddress The desired streaming target.
	/// @param taxToken The tax token tied to this vault.
	/// @param xHandle The fee manager's X handle.
	/// @param XId The X (Twitter) account id.
	/// @param tweetId The tweet id used as proof.
	struct XProof {
		address targetAddress;
		address taxToken;
		string xHandle;
		uint128 XId;
		uint128 tweetId;
	}

	/// @notice Emitted when the vault state changes.
	/// @param token The tax token address.
	/// @param newState The new state (0: ACCUMULATING, 1: STREAMING, 2: FALLBACK_SNOWBALL).
	event FlapTaxVaultStateChanged(address token, uint8 newState);

	/// @notice Emitted when the streaming target is updated.
	/// @param token The tax token address.
	/// @param newTarget The new streaming target address.
	event FlapTaxVaultStreamingTargetUpdated(address token, address newTarget);

	/// @notice Emitted when snowball balance is updated.
	/// @param token The tax token address.
	/// @param vault The vault address.
	/// @param newBalance The new BNB balance.
	/// @param updateType The type of update (ACCUMULATION or SNOWBALL).
	event FlapSnowballBalanceUpdated(address token, address vault, uint256 newBalance, BalanceUpdateType updateType);

	/// @notice Emitted when forwarding to streaming target fails.
	/// @param token The tax token address.
	/// @param target The target address that rejected the transfer.
	/// @param amount The amount that failed to transfer.
	event FlapStreamingForwardFailed(address token, address target, uint256 amount);

	/// @notice Error when trying to use an outdated proof.
	/// @param providedTweetId The tweet id provided in the proof.
	/// @param lastTweetId The latest stored tweet id.
	error OutdatedProof(uint128 providedTweetId, uint128 lastTweetId);

	/// @notice Error when the vault is in the wrong state for the operation.
	/// @param currentState The current vault state.
	error InvalidState(State currentState);

	/// @notice Error when xHandle is empty.
	error EmptyXHandle();

	/// @notice Error when proof verification fails.
	error InvalidProof();

	/// @notice Error when proof tax token does not match this vault.
	error MismatchedTaxToken();

	/// @notice Error when proof xHandle does not match this vault.
	error MismatchedXHandle();

	/// @notice Error when attempting to revoke the guardian role.
	error CannotRevokeGuardianRole();

	/// @notice Role allowed to execute snowball buybacks.
	function SNOWBALL_ROLE() external view returns (bytes32);

	/// @notice Dead address used for burns.
	function DEAD_ADDRESS() external view returns (address);

	/// @notice Initialize the vault after cloning.
	/// @param _taxToken The tax token address.
	/// @param _quoteToken The quote token address.
	/// @param _xHandle The fee manager's X handle.
	/// @param _timeoutPeriod Time before fallback state (seconds).
	function initialize(address _taxToken, address _quoteToken, string calldata _xHandle, uint256 _timeoutPeriod)
		external;

	/// @notice Current computed vault state.
	/// @return The current state (computed from storage and time).
	function state() external view returns (State);

	/// @notice Transition state if timeout rules are met.
	function transitState() external;

	/// @notice Manage the vault using a signed X proof.
	/// @param proof The proof payload.
	/// @param signature The EIP-712 signature.
	function manageByProof(XProof calldata proof, bytes calldata signature) external;

	/// @notice Buy back and burn tax tokens using accumulated BNB.
	/// @param quoteAmt The amount of BNB to use for buyback.
	function snowball(uint256 quoteAmt) external;

	/// @notice Return historical proofs with pagination.
	/// @param offset Starting index for pagination (0 = most recent).
	/// @param limit Maximum number of records to return.
	/// @return records Array of proof records in descending order (newest first).
	/// @return total Total number of historical proofs.
	function getHistoricalProofs(uint256 offset, uint256 limit)
		external
		view
		returns (ProofRecord[] memory records, uint256 total);

	/// @notice Returns a human-readable description of the vault.
	/// @return A string describing the vault state and key metrics.
	function description() external view returns (string memory);

	/// @notice Override of AccessControl's revokeRole to protect guardian.
	/// @param role The role to revoke.
	/// @param account The account to revoke the role from.
	function revokeRole(bytes32 role, address account) external;

	/// @notice Current aggregate stats.
	function stats() external view returns (uint128 totalBNBSpent, uint128 totalTokenBurn);

	/// @notice The tax token associated with this vault.
	function taxToken() external view returns (address);

	/// @notice The quote token used for snowball swaps.
	function quoteToken() external view returns (address);

	/// @notice The fee manager's X handle.
	function xHandle() external view returns (string memory);

	/// @notice Timeout period before fallback.
	function timeoutPeriod() external view returns (uint256);

	/// @notice Creation timestamp for this vault.
	function createdAt() external view returns (uint256);

	/// @notice Vault factory address.
	function factory() external view returns (address);

	/// @notice Current streaming target (only in STREAMING state).
	function streamingTarget() external view returns (address);

	/// @notice Total streamed amount for a beneficiary.
	/// @param beneficiary The beneficiary address.
	function streamedAmount(address beneficiary) external view returns (uint256);

	/// @notice Latest recorded tweet id used for monotonicity checks.
	function lastTweetId() external view returns (uint128);
}
```
