Skip to main content
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

Obtaining Payment Proofs

1

Complete the fiat payment

Send fiat through the payment method specified in your intent:
// 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.
2

Request attestation

Submit your payment receipt to the attestation service:
// 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);
3

Prepare verification data

Format the attestation for on-chain verification:
// Attestation is an EIP-712 signed message
const paymentProof = attestation.signature; // The signed attestation bytes
const verificationData = attestation.metadata || "0x"; // Additional data

Fulfilling the Intent

Basic Fulfillment

Submit the payment proof to complete the trade:
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:
// 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
});
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.).

Payment Verification

The UnifiedPaymentVerifier performs these checks:
1

Signature validation

Verifies the EIP-712 signature on the attestation:
  • Signature matches the attester’s public key
  • Attestation hasn’t been tampered with
2

Payment data validation

Checks the payment details:
  • Intent hash matches
  • Payment amount is correct
  • Payee details match the deposit
  • Currency matches
3

Nullifier check

Prevents double-spending:
  • Payment ID hasn’t been used before
  • Marks payment as consumed in NullifierRegistry
4

Timestamp validation

Ensures proof freshness:
  • Attestation timestamp is recent enough
  • Allows buffer time for L2 block delays

Fee Distribution

Fees are automatically deducted and distributed during fulfillment:
// 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:
// 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
Only the deposit owner can call releaseFundsToPayer. This is a safety mechanism for exceptional circumstances.

Reading Fulfillment Status

// 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

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));
    }
  }
}

Common Errors

ErrorCauseSolution
IntentNotFoundIntent already completed or canceledCheck intent status before fulfilling
PaymentVerificationFailedInvalid attestation or signatureRequest new attestation from service
HashMismatchProof’s intentHash doesn’t matchEnsure you’re using correct intentHash
AmountBelowMinPartial fulfillment below minimumNot supported in v2.1 - must fulfill full amount
Nullifier already usedPayment already submittedEach payment can only be used once

Anyone Can Fulfill

Important: Anyone can submit a fulfillment transaction, not just the intent owner:
// 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

Verify Before Paying

Confirm your intent was signaled successfully before sending fiat.

Save Payment Receipt

Keep records of your fiat payment for attestation requests.

Act Before Expiry

Fulfill before the intent expires (typically 24 hours).

Monitor Events

Watch for IntentFulfilled event to confirm successful completion.

Next Steps

Using Hooks

Learn about post-intent hooks for advanced workflows

Testing

Test the full flow in a local environment

Build docs developers (and LLMs) love