Skip to main content

Overview

An intent represents a buyer’s commitment to pay off-chain fiat currency in exchange for on-chain tokens. This page walks through the complete lifecycle from signal to fulfillment, showing all contract interactions and state changes.
The intent lifecycle is the core workflow of the zkp2p protocol, coordinating between Orchestrator, Escrow, payment verifiers, and registries.

Lifecycle Stages

Stage 1: Discovery

The buyer browses available deposits (off-chain indexing or on-chain queries) to find one matching their requirements:
// Off-chain: Query deposits via event indexing or ProtocolViewer
const deposits = await protocolViewer.getActiveDeposits();

// Find deposits that:
// - Accept the buyer's payment method (e.g., Venmo)
// - Support the buyer's currency (e.g., USD)
// - Have sufficient liquidity
// - Offer acceptable conversion rates
const matchingDeposits = deposits.filter(d => 
    d.paymentMethods.includes(venmoHash) &&
    d.currencies[venmoHash].includes(usdHash) &&
    d.remainingDeposits >= desiredAmount &&
    d.conversionRates[venmoHash][usdHash] >= minAcceptableRate
);

Stage 2: Gating Service (Optional)

If the deposit has a intentGatingService, the buyer must obtain a signature:

Request Signature

// Buyer calls gating service API (off-chain)
const gatingRequest = {
    orchestrator: orchestratorAddress,
    escrow: escrowAddress,
    depositId: depositId,
    amount: intentAmount,
    to: buyerAddress,
    paymentMethod: venmoHash,
    fiatCurrency: usdHash,
    conversionRate: agreedRate,
    signatureExpiration: timestamp + 3600, // 1 hour validity
    chainId: chainId
};

const signature = await gatingService.requestSignature(gatingRequest);

Gating Service Validation

The gating service may:
  • Perform KYC/AML checks
  • Verify the buyer is on an allowlist
  • Check reputation scores
  • Rate-limit signaling frequency
  • Enforce geographic restrictions
If the deposit has no gating service (intentGatingService == address(0)), this stage is skipped.

Stage 3: Signal Intent

The buyer calls signalIntent() on the Orchestrator.

Transaction Flow

Code Example

IOrchestrator.SignalIntentParams memory params = IOrchestrator.SignalIntentParams({
    escrow: escrowAddress,
    depositId: depositId,
    amount: 1000e6,  // 1000 USDC
    to: buyerAddress,
    paymentMethod: keccak256("venmo"),
    fiatCurrency: keccak256("USD"),
    conversionRate: 1.02e18,  // 1.02 USD per USDC
    referrer: referrerAddress,
    referrerFee: 1e16,  // 1%
    gatingServiceSignature: signature,
    signatureExpiration: expiration,
    postIntentHook: IPostIntentHook(bridgeHook),
    data: bridgeHookData
});

orchestrator.signalIntent(params);

State Changes

Orchestrator:
  • Creates Intent struct with all parameters
  • Adds intent hash to accountIntents[buyer]
  • Stores intentMinAtSignal[intentHash] (deposit’s min amount at signal time)
  • Increments intentCounter
Escrow:
  • Validates deposit is accepting intents
  • Validates amount is within deposit’s intentAmountRange
  • Prunes expired intents if liquidity is needed
  • Moves amount from remainingDeposits to outstandingIntentAmount
  • Creates Intent struct with expiryTime = timestamp + intentExpirationPeriod
  • Adds intent hash to depositIntentHashes[depositId]

Validation Checks

The Orchestrator performs extensive validation:
  1. Multiple Intent Check: Buyer must be whitelisted relayer OR allowMultipleIntents == true
  2. Zero Address Check: to address must not be zero
  3. Fee Validation: Referrer fee ≤ 50%, and must be 0 if no referrer
  4. Hook Validation: If post-intent hook set, must be whitelisted
  5. Escrow Validation: Must be whitelisted OR registry accepts all escrows
  6. Payment Method: Must exist in PaymentVerifierRegistry and be active on deposit
  7. Currency: Must be supported by payment method with non-zero min rate
  8. Conversion Rate: Must be ≥ deposit’s min conversion rate
  9. Gating Signature: If deposit has gating service, signature must be valid and not expired

Stage 4: Awaiting Payment

After signaling, the buyer has intentExpirationPeriod (configured on Escrow) to complete the off-chain payment.

Buyer Actions

  1. Make Off-Chain Payment: Send fiat to the depositor’s payment account (Venmo username, PayPal email, etc.)
  2. Include Intent Hash: Some payment methods allow notes/memos - buyer should include intent hash
  3. Wait for Confirmation: Payment service confirms transaction

Intent Status

// Query intent on Orchestrator
IOrchestrator.Intent memory intent = orchestrator.getIntent(intentHash);

// Check if expired
IEscrow.Intent memory escrowIntent = escrow.getDepositIntent(depositId, intentHash);
bool isExpired = block.timestamp > escrowIntent.expiryTime;

Stage 5: Payment Attestation

Once the off-chain payment is confirmed, an attestation service generates a cryptographic proof.

Attestation Service Flow

Constructing the Attestation

// Extract from payment service
const paymentDetails = {
    method: keccak256("venmo"),
    payeeId: keccak256(depositorVenmoUsername),
    amount: 1020, // 1020 cents = $10.20
    currency: keccak256("USD"),
    timestamp: paymentTimestampMillis,
    paymentId: keccak256(venmoTransactionId)
};

// Fetch intent data
const intent = await orchestrator.getIntent(intentHash);

// Create snapshot
const intentSnapshot = {
    intentHash: intentHash,
    amount: intent.amount,
    paymentMethod: intent.paymentMethod,
    fiatCurrency: intent.fiatCurrency,
    payeeDetails: intent.payeeId,
    conversionRate: intent.conversionRate,
    signalTimestamp: intent.timestamp,
    timestampBuffer: 3600000  // 1 hour in milliseconds
};

// Calculate release amount (FX conversion)
const fiatPaidCents = paymentDetails.amount;
const fiatPaidUnits = fiatPaidCents / 100;  // 10.20 USD
const conversionRate = intent.conversionRate / 1e18;  // 1.02
const releaseAmount = Math.floor((fiatPaidUnits / conversionRate) * 1e6);  // USDC (6 decimals)

// Encode data
const data = ethers.utils.defaultAbiCoder.encode(
    ["tuple(bytes32,bytes32,uint256,bytes32,uint256,bytes32)", 
     "tuple(bytes32,uint256,bytes32,bytes32,bytes32,uint256,uint256,uint256)"],
    [paymentDetails, intentSnapshot]
);
const dataHash = keccak256(data);

// Construct EIP-712 digest
const domain = {
    name: "UnifiedPaymentVerifier",
    version: "1",
    chainId: chainId,
    verifyingContract: verifierAddress
};

const types = {
    PaymentAttestation: [
        { name: "intentHash", type: "bytes32" },
        { name: "releaseAmount", type: "uint256" },
        { name: "dataHash", type: "bytes32" }
    ]
};

const value = {
    intentHash: intentHash,
    releaseAmount: releaseAmount,
    dataHash: dataHash
};

// Sign with attestor private key(s)
const signature = await attestor._signTypedData(domain, types, value);

// Construct final attestation
const attestation = {
    intentHash: intentHash,
    releaseAmount: releaseAmount,
    dataHash: dataHash,
    signatures: [signature],  // Can be multiple for threshold attestation
    data: data,
    metadata: "0x"  // Optional additional data not signed
};

Stage 6: Fulfill Intent

Anyone can submit the attestation to fulfill the intent.

Transaction Flow

Code Example

IOrchestrator.FulfillIntentParams memory params = IOrchestrator.FulfillIntentParams({
    paymentProof: abi.encode(attestation),
    intentHash: intentHash,
    verificationData: "",  // Additional data for verifier (if needed)
    postIntentHookData: bridgeData  // Data for post-intent hook
});

orchestrator.fulfillIntent(params);

State Changes

Verifier:
  • Validates attestation signatures
  • Adds payment ID to NullifierRegistry
  • Emits PaymentVerified event
Orchestrator:
  • Deletes Intent from storage
  • Removes intent hash from accountIntents
  • Deletes intentMinAtSignal
  • Emits IntentPruned and IntentFulfilled events
Escrow:
  • Moves amount from outstandingIntentAmount back to available balance
  • Deletes Intent struct
  • Removes intent hash from depositIntentHashes
  • Transfers tokens to Orchestrator
  • May close deposit if dust threshold reached
  • Emits FundsUnlockedAndTransferred event

Fee Distribution

// Protocol fee (e.g., 1%)
protocolFeeAmount = (releaseAmount * protocolFee) / 1e18;
transfer(protocolFeeRecipient, protocolFeeAmount);

// Referrer fee (e.g., 1%)
referrerFeeAmount = (releaseAmount * intent.referrerFee) / 1e18;
transfer(intent.referrer, referrerFeeAmount);

// Net amount to buyer
netAmount = releaseAmount - protocolFeeAmount - referrerFeeAmount;
transfer(intent.to, netAmount);  // Or to post-intent hook

Alternative Paths

Path A: Cancellation

If the buyer changes their mind before making payment:
orchestrator.cancelIntent(intentHash);
Flow:
  1. Orchestrator validates caller is intent owner
  2. Orchestrator deletes intent from storage
  3. Orchestrator calls escrow.unlockFunds(depositId, intentHash)
  4. Escrow returns amount to remainingDeposits
  5. Escrow deletes intent from storage
Use Cases:
  • Buyer found better rate elsewhere
  • Payment service is down
  • Buyer doesn’t have sufficient fiat balance

Path B: Expiration

If the intent is not fulfilled before expiryTime:
// Anyone can call to cleanup expired intents
escrow.pruneExpiredIntents(depositId);
Flow:
  1. Escrow iterates through depositIntentHashes[depositId]
  2. For each expired intent (block.timestamp > expiryTime):
    • Return amount to remainingDeposits
    • Delete intent from storage
    • Remove from depositIntentHashes
    • Emit FundsUnlocked event
  3. Escrow calls orchestrator.pruneIntents(expiredIntents)
  4. Orchestrator deletes each intent from storage
Automatic Pruning: Expired intents are automatically pruned when:
  • Depositor calls removeFunds() or withdrawDeposit()
  • New intent is signaled and liquidity is needed
  • Anyone calls pruneExpiredIntents()

Path C: Manual Release

If there’s a dispute or special arrangement, the depositor can manually release funds:
orchestrator.releaseFundsToPayer(intentHash);
Flow:
  1. Orchestrator validates caller is the deposit’s depositor
  2. Orchestrator deletes intent from storage
  3. Orchestrator calls escrow.unlockAndTransferFunds() with full intent amount
  4. Escrow transfers tokens to Orchestrator
  5. Orchestrator calculates and transfers fees
  6. Orchestrator transfers net amount to buyer
  7. Emit IntentFulfilled(intentHash, to, netAmount, isManualRelease: true)
Security: Manual release still charges protocol and referrer fees. Depositors cannot bypass fee collection.

Intent States Summary

StateDescriptionCan Cancel?Can Fulfill?Liquidity
SignaledIntent active, awaiting paymentYes (owner)Yes (anyone)Locked
ExpiredPast expiryTime, not yet prunedNoNoLocked
CancelledOwner cancelled before fulfillmentN/AN/AReleased
FulfilledPayment verified and releasedN/AN/ATransferred
PrunedExpired and cleaned upN/AN/AReleased
ReleasedManually released by depositorN/AN/ATransferred

Timeline Example

T=0:00    Buyer signals intent
          └─ Orchestrator: Intent created, hash = 0xabc...
          └─ Escrow: 1000 USDC locked

T=0:05    Buyer makes Venmo payment
          └─ Off-chain: $1020 sent to depositor

T=0:10    Attestation service detects payment
          └─ Off-chain: Verify payment matches intent
          └─ Off-chain: Generate EIP-712 signature

T=0:15    Relayer submits fulfillment
          └─ Orchestrator: verifyPayment()
          └─ Verifier: Validate attestation ✓
          └─ Nullifier: Add payment ID ✓
          └─ Escrow: Release 1000 USDC
          └─ Fees: 10 USDC protocol, 10 USDC referrer
          └─ Buyer receives: 980 USDC

T=0:15    Intent fulfilled ✓

Best Practices

Monitor Expiry

Buyers should complete payment well before expiryTime to account for attestation delays

Include Intent Hash

If payment service supports memos, include intent hash for faster attestation matching

Use Relayers

Buyers can outsource fulfillment submission to relayers to save gas and improve UX

Verify Deposit

Before signaling, verify deposit has sufficient liquidity and acceptable parameters

Build docs developers (and LLMs) love