> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/zkp2p/zkp2p-contracts/llms.txt
> Use this file to discover all available pages before exploring further.

# Fulfilling Intents with Payment Proofs

> Learn how to submit payment proofs and complete trades in the ZKP2P protocol

After signaling an intent and sending fiat payment, you must submit a payment proof to unlock your USDC. This guide covers the fulfillment process.

## Overview

Fulfilling an intent involves:

1. Obtaining a zkTLS attestation proving your off-chain payment
2. Submitting the proof to the Orchestrator contract
3. Automatic verification and fund transfer
4. Optional post-intent hook execution

The **Orchestrator** coordinates verification through the **UnifiedPaymentVerifier**, which validates EIP-712 signed attestations.

## Payment Proof Flow

```mermaid theme={null}
sequenceDiagram
    participant Taker
    participant PaymentApp as Payment App<br/>(Venmo/PayPal/etc)
    participant AttestationService as Attestation Service
    participant Orchestrator
    participant Verifier as UnifiedPaymentVerifier
    participant Escrow
    
    Taker->>PaymentApp: Send fiat payment
    PaymentApp-->>Taker: Payment confirmation
    Taker->>AttestationService: Request zkTLS proof
    AttestationService->>PaymentApp: Verify payment (zkTLS)
    AttestationService-->>Taker: Signed attestation
    Taker->>Orchestrator: fulfillIntent(proof)
    Orchestrator->>Verifier: verifyPayment()
    Verifier-->>Orchestrator: Verification result
    Orchestrator->>Escrow: unlockAndTransferFunds()
    Escrow-->>Orchestrator: USDC transferred
    Orchestrator-->>Taker: USDC received
```

## Obtaining Payment Proofs

<Steps>
  <Step title="Complete the fiat payment">
    Send fiat through the payment method specified in your intent:

    ```typescript theme={null}
    // From your intent
    const intent = await orchestrator.getIntent(intentHash);

    // Calculate exact fiat amount to send
    const fiatAmount = intent.amount
      .mul(intent.conversionRate)
      .div(ethers.utils.parseEther("1"));

    const amountToSend = ethers.utils.formatUnits(fiatAmount, 6);
    console.log(`Send ${amountToSend} to the maker via payment app`);
    ```

    Make the payment through your Venmo/PayPal/etc. app to the maker's account.
  </Step>

  <Step title="Request attestation">
    Submit your payment receipt to the attestation service:

    ```typescript theme={null}
    // This is a simplified example - actual implementation varies by service
    const attestationResponse = await fetch(
      'https://attestation-service.zkp2p.xyz/attest',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          intentHash: intentHash,
          paymentMethod: 'venmo',
          transactionId: 'venmo-tx-id-from-receipt',
          amount: amountToSend,
          // Additional payment proof data
        })
      }
    );

    const { attestation } = await attestationResponse.json();
    console.log('Received attestation:', attestation);
    ```
  </Step>

  <Step title="Prepare verification data">
    Format the attestation for on-chain verification:

    ```typescript theme={null}
    // Attestation is an EIP-712 signed message
    const paymentProof = attestation.signature; // The signed attestation bytes
    const verificationData = attestation.metadata || "0x"; // Additional data
    ```
  </Step>
</Steps>

## Fulfilling the Intent

### Basic Fulfillment

Submit the payment proof to complete the trade:

```typescript theme={null}
import { Orchestrator } from "@typechain/Orchestrator";

const orchestrator = new ethers.Contract(
  ORCHESTRATOR_ADDRESS,
  Orchestrator_ABI,
  signer
) as Orchestrator;

const tx = await orchestrator.fulfillIntent({
  intentHash: intentHash,
  paymentProof: paymentProof, // EIP-712 signed attestation
  verificationData: verificationData, // Additional verification data
  postIntentHookData: "0x" // Data for post-intent hook (if any)
});

const receipt = await tx.wait();
console.log("Intent fulfilled!", receipt.transactionHash);

// Check the IntentFulfilled event
const event = receipt.events?.find(e => e.event === "IntentFulfilled");
const amountReceived = event?.args?.amount;
const recipient = event?.args?.fundsTransferredTo;

console.log(`Received ${ethers.utils.formatUnits(amountReceived, 6)} USDC`);
console.log(`Funds sent to: ${recipient}`);
```

### Fulfillment with Post-Intent Hooks

If your intent specified a post-intent hook (e.g., for bridging), provide hook-specific data:

```typescript theme={null}
// Example: AcrossBridge hook data for bridging to another chain
const hookData = ethers.utils.defaultAbiCoder.encode(
  [
    "tuple(bytes32 intentHash, uint256 outputAmount, uint32 fillDeadlineOffset, bytes32 exclusiveRelayer, uint32 exclusivityParameter)"
  ],
  [{
    intentHash: intentHash,
    outputAmount: ethers.utils.parseUnits("49.5", 6), // Expected output after bridge fees
    fillDeadlineOffset: 21600, // 6 hours
    exclusiveRelayer: "0x1562A70707D62edBF3a90317E46E1DF075E2d924", // Padded to bytes32
    exclusivityParameter: 5 // 5 seconds
  }]
);

await orchestrator.fulfillIntent({
  intentHash: intentHash,
  paymentProof: paymentProof,
  verificationData: verificationData,
  postIntentHookData: hookData // Hook-specific data
});
```

<Note>
  When using post-intent hooks, the USDC is transferred to the hook contract instead of directly to `intent.to`. The hook then executes custom logic (bridging, swapping, etc.).
</Note>

## Payment Verification

The UnifiedPaymentVerifier performs these checks:

<Steps>
  <Step title="Signature validation">
    Verifies the EIP-712 signature on the attestation:

    * Signature matches the attester's public key
    * Attestation hasn't been tampered with
  </Step>

  <Step title="Payment data validation">
    Checks the payment details:

    * Intent hash matches
    * Payment amount is correct
    * Payee details match the deposit
    * Currency matches
  </Step>

  <Step title="Nullifier check">
    Prevents double-spending:

    * Payment ID hasn't been used before
    * Marks payment as consumed in NullifierRegistry
  </Step>

  <Step title="Timestamp validation">
    Ensures proof freshness:

    * Attestation timestamp is recent enough
    * Allows buffer time for L2 block delays
  </Step>
</Steps>

## Fee Distribution

Fees are automatically deducted and distributed during fulfillment:

```typescript theme={null}
// Example: 100 USDC intent with 1% protocol fee and 0.5% referrer fee
const intentAmount = ethers.utils.parseUnits("100", 6);
const protocolFee = intentAmount.mul(100).div(10000); // 1 USDC
const referrerFee = intentAmount.mul(50).div(10000);  // 0.5 USDC
const netAmount = intentAmount.sub(protocolFee).sub(referrerFee); // 98.5 USDC

// After fulfillment:
// - Protocol fee recipient receives 1 USDC
// - Referrer receives 0.5 USDC
// - intent.to receives 98.5 USDC (or hook receives and processes it)
```

## Manual Release by Depositor

In case of issues, the depositor can manually release funds to you:

```typescript theme={null}
// Depositor calls this if fulfillment fails but you completed payment
await orchestrator.connect(depositorSigner).releaseFundsToPayer(intentHash);

// This transfers the full intent amount (minus fees) directly to intent.to
// Bypasses payment verification
```

<Warning>
  Only the deposit owner can call `releaseFundsToPayer`. This is a safety mechanism for exceptional circumstances.
</Warning>

## Reading Fulfillment Status

```typescript theme={null}
// Check if intent still exists (unfulfilled)
try {
  const intent = await orchestrator.getIntent(intentHash);
  if (intent.timestamp.isZero()) {
    console.log("Intent does not exist (fulfilled, canceled, or pruned)");
  } else {
    console.log("Intent still active");
  }
} catch (error) {
  console.log("Intent completed or canceled");
}

// Check escrow state
const deposit = await escrow.getDeposit(depositId);
const escrowIntent = await escrow.getDepositIntent(depositId, intentHash);

if (escrowIntent.intentHash === ethers.constants.HashZero) {
  console.log("Intent no longer locked in escrow");
}
```

## Handling Errors

<CodeGroup>
  ```typescript Retry Logic theme={null}
  async function fulfillWithRetry(
    intentHash: string,
    paymentProof: string,
    maxRetries = 3
  ) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        const tx = await orchestrator.fulfillIntent({
          intentHash,
          paymentProof,
          verificationData: "0x",
          postIntentHookData: "0x"
        });
        
        const receipt = await tx.wait();
        console.log("Fulfilled on attempt", i + 1);
        return receipt;
      } catch (error) {
        console.error(`Attempt ${i + 1} failed:`, error.message);
        
        if (i === maxRetries - 1) throw error;
        
        // Wait before retry
        await new Promise(resolve => setTimeout(resolve, 5000));
      }
    }
  }
  ```

  ```typescript Error Handling theme={null}
  try {
    await orchestrator.fulfillIntent(params);
  } catch (error) {
    if (error.message.includes("IntentNotFound")) {
      console.error("Intent already fulfilled or canceled");
    } else if (error.message.includes("PaymentVerificationFailed")) {
      console.error("Payment proof is invalid or expired");
    } else if (error.message.includes("HashMismatch")) {
      console.error("Intent hash in proof doesn't match");
    } else {
      console.error("Unknown error:", error.message);
    }
  }
  ```
</CodeGroup>

## Common Errors

| Error                       | Cause                                | Solution                                         |
| --------------------------- | ------------------------------------ | ------------------------------------------------ |
| `IntentNotFound`            | Intent already completed or canceled | Check intent status before fulfilling            |
| `PaymentVerificationFailed` | Invalid attestation or signature     | Request new attestation from service             |
| `HashMismatch`              | Proof's intentHash doesn't match     | Ensure you're using correct intentHash           |
| `AmountBelowMin`            | Partial fulfillment below minimum    | Not supported in v2.1 - must fulfill full amount |
| Nullifier already used      | Payment already submitted            | Each payment can only be used once               |

## Anyone Can Fulfill

Important: **Anyone** can submit a fulfillment transaction, not just the intent owner:

```typescript theme={null}
// Relayer can fulfill on behalf of taker
await orchestrator.connect(relayerSigner).fulfillIntent({
  intentHash: intentHash, // Taker's intent
  paymentProof: paymentProof, // Taker's proof
  verificationData: verificationData,
  postIntentHookData: "0x"
});

// Funds still go to intent.to (the taker)
// This enables gasless fulfillment via relayers
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Verify Before Paying" icon="shield-check">
    Confirm your intent was signaled successfully before sending fiat.
  </Card>

  <Card title="Save Payment Receipt" icon="receipt">
    Keep records of your fiat payment for attestation requests.
  </Card>

  <Card title="Act Before Expiry" icon="timer">
    Fulfill before the intent expires (typically 24 hours).
  </Card>

  <Card title="Monitor Events" icon="bell">
    Watch for `IntentFulfilled` event to confirm successful completion.
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Using Hooks" icon="plug" href="/guides/hooks">
    Learn about post-intent hooks for advanced workflows
  </Card>

  <Card title="Testing" icon="flask" href="/guides/testing">
    Test the full flow in a local environment
  </Card>
</CardGroup>
