Skip to main content
As a taker (buyer), you signal intents to purchase USDC from maker deposits in exchange for fiat payments. This guide explains how to create and manage intents.

Overview

Signaling an intent:
  1. Locks liquidity from a maker’s deposit on the Escrow contract
  2. Creates a commitment to send fiat payment off-chain
  3. Records all trade parameters on-chain for later verification
  4. Starts an expiration timer (typically 24 hours)
Intents are created through the Orchestrator contract which coordinates the entire trade lifecycle.

Finding Available Deposits

Before signaling an intent, query available deposits:
import { ethers } from "ethers";
import { ProtocolViewer } from "@typechain/ProtocolViewer";

const PROTOCOL_VIEWER_ADDRESS = "0x...";
const viewer = new ethers.Contract(
  PROTOCOL_VIEWER_ADDRESS,
  ProtocolViewer_ABI,
  provider
) as ProtocolViewer;

// Get all deposits with available liquidity
const deposits = await viewer.getDeposits();

// Filter for deposits that:
// - Are accepting intents
// - Have sufficient liquidity
// - Support your desired payment method
const venmoHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("venmo"));

const availableDeposits = deposits.filter(d => 
  d.deposit.acceptingIntents &&
  d.deposit.remainingDeposits.gte(ethers.utils.parseUnits("50", 6)) &&
  d.paymentMethods.some(pm => pm.paymentMethod === venmoHash && pm.active)
);

console.log(`Found ${availableDeposits.length} available deposits`);

Signaling an Intent

1

Prepare intent parameters

Gather all required information for the trade:
import { Orchestrator } from "@typechain/Orchestrator";

const ORCHESTRATOR_ADDRESS = "0x...";
const ESCROW_ADDRESS = "0x...";

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

// Trade parameters
const depositId = 0; // From your deposit search
const amount = ethers.utils.parseUnits("50", 6); // 50 USDC
const recipientAddress = await signer.getAddress(); // Where to receive USDC

// Payment details
const paymentMethod = ethers.utils.keccak256(
  ethers.utils.toUtf8Bytes("venmo")
);
const fiatCurrency = ethers.utils.keccak256(
  ethers.utils.toUtf8Bytes("USD")
);

// Conversion rate (must meet deposit's minimum)
const conversionRate = ethers.utils.parseEther("1.02"); // 1 USDC = 1.02 USD
2

Get gating signature (if required)

If the deposit requires intent gating, obtain a signature from the gating service:
// Check if gating is required
const escrow = new ethers.Contract(ESCROW_ADDRESS, Escrow_ABI, provider);
const gatingService = await escrow.getDepositGatingService(
  depositId,
  paymentMethod
);

let gatingSignature = "0x";
let signatureExpiration = 0;

if (gatingService !== ethers.constants.AddressZero) {
  // Request signature from gating service API
  const response = await fetch('https://gating-service.example.com/sign', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      orchestrator: ORCHESTRATOR_ADDRESS,
      escrow: ESCROW_ADDRESS,
      depositId,
      amount: amount.toString(),
      to: recipientAddress,
      paymentMethod,
      fiatCurrency,
      conversionRate: conversionRate.toString()
    })
  });
  
  const data = await response.json();
  gatingSignature = data.signature;
  signatureExpiration = data.expiration;
}
3

Call signalIntent

Submit the intent transaction:
const tx = await orchestrator.signalIntent({
  escrow: ESCROW_ADDRESS,
  depositId: depositId,
  amount: amount,
  to: recipientAddress,
  paymentMethod: paymentMethod,
  fiatCurrency: fiatCurrency,
  conversionRate: conversionRate,
  referrer: ethers.constants.AddressZero, // Optional referrer
  referrerFee: 0, // Fee for referrer (0-50% in 18 decimals)
  gatingServiceSignature: gatingSignature,
  signatureExpiration: signatureExpiration || 0,
  postIntentHook: ethers.constants.AddressZero, // Optional hook
  data: "0x" // Hook-specific data
});

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

// Extract intentHash from event
const event = receipt.events?.find(e => e.event === "IntentSignaled");
const intentHash = event?.args?.intentHash;
console.log("Intent hash:", intentHash);

Signaling with Referrers

Include a referrer to share fees:
const REFERRER_ADDRESS = "0x...";
const REFERRER_FEE = ethers.utils.parseEther("0.01"); // 1% fee to referrer

await orchestrator.signalIntent({
  escrow: ESCROW_ADDRESS,
  depositId: depositId,
  amount: amount,
  to: recipientAddress,
  paymentMethod: paymentMethod,
  fiatCurrency: fiatCurrency,
  conversionRate: conversionRate,
  referrer: REFERRER_ADDRESS,
  referrerFee: REFERRER_FEE, // Paid from your USDC amount
  gatingServiceSignature: gatingSignature,
  signatureExpiration: signatureExpiration,
  postIntentHook: ethers.constants.AddressZero,
  data: "0x"
});
Referrer fees are capped at 50% and are deducted from the USDC you receive. For example, with a 1% referrer fee on 100 USDC, you receive ~99 USDC (after protocol fees too).

Multiple Intents

By default, accounts can only have one active intent at a time. To signal multiple intents:
// Check if multiple intents are allowed globally
const allowMultiple = await orchestrator.allowMultipleIntents();

// Or check if you're a whitelisted relayer
const relayerRegistry = await orchestrator.relayerRegistry();
const registry = new ethers.Contract(
  relayerRegistry,
  RelayerRegistry_ABI,
  provider
);
const isRelayer = await registry.isWhitelistedRelayer(await signer.getAddress());

if (!allowMultiple && !isRelayer) {
  console.log("Can only have one active intent");
}

Reading Intent State

// Get intent details
const intent = await orchestrator.getIntent(intentHash);

console.log("Owner:", intent.owner);
console.log("Recipient:", intent.to);
console.log("Amount:", ethers.utils.formatUnits(intent.amount, 6), "USDC");
console.log("Timestamp:", new Date(intent.timestamp.toNumber() * 1000));
console.log("Payment method:", intent.paymentMethod);
console.log("Currency:", intent.fiatCurrency);
console.log("Rate:", ethers.utils.formatEther(intent.conversionRate));

// Calculate fiat amount to send
const fiatAmount = intent.amount
  .mul(intent.conversionRate)
  .div(ethers.utils.parseEther("1"));
console.log("Send fiat:", ethers.utils.formatUnits(fiatAmount, 6));

// Get all your active intents
const myIntents = await orchestrator.getAccountIntents(
  await signer.getAddress()
);
console.log("Active intents:", myIntents);

Canceling an Intent

Cancel before making the fiat payment if you change your mind:
try {
  const tx = await orchestrator.cancelIntent(intentHash);
  await tx.wait();
  console.log("Intent canceled, liquidity returned to deposit");
} catch (error) {
  console.error("Failed to cancel:", error.message);
}
You can only cancel intents you own. Once you’ve sent the fiat payment and obtained a proof, you should fulfill the intent instead of canceling.

After Signaling

Once your intent is signaled:
1

Send fiat payment

Use the payment method specified in your intent to send fiat to the maker’s payee details.Amount to send: Calculate from the intent’s amount and conversion rate.
2

Obtain payment proof

Get a zkTLS attestation or proof from the attestation service proving your payment.
3

Fulfill the intent

Submit the payment proof to unlock your USDC. See Fulfilling Intents.

Validation Checks

The Orchestrator performs these validations when signaling:
  • Deposit exists and is accepting intents
  • Sufficient liquidity available
  • Amount within deposit’s intent range
  • Payment method is registered in PaymentVerifierRegistry
  • Payment method is active for this deposit
  • Currency is supported by the payment method
  • Conversion rate meets deposit’s minimum
  • Gating signature is valid (if required)
  • Signature hasn’t expired
  • Account can have multiple intents (if not first)
  • Referrer fee ≤ 50%
  • Referrer address is set if fee > 0
  • Post-intent hook is whitelisted (if specified)

Common Errors

ErrorCauseSolution
InsufficientDepositLiquidityDeposit doesn’t have enough available fundsChoose a different deposit or reduce amount
AmountBelowMinAmount less than deposit’s minimumIncrease your intent amount
AmountAboveMaxAmount exceeds deposit’s maximumReduce your intent amount
RateBelowMinimumConversion rate too lowIncrease conversion rate to match deposit
AccountHasActiveIntentYou already have an active intentCancel existing intent or wait for it to complete
InvalidSignatureGating signature is invalid or expiredRequest a new signature from gating service

Best Practices

Check Rates

Verify the conversion rate meets your expectations before signaling.

Act Quickly

Signal and fulfill promptly - intents typically expire in 24 hours.

Save Intent Hash

Store the intentHash from the event - you’ll need it for fulfillment.

Monitor Expiry

Track when your intent expires and fulfill before the deadline.

Next Steps

Fulfill Intent

Learn how to submit payment proofs and receive your USDC

Contract Reference

View the complete Orchestrator contract documentation

Build docs developers (and LLMs) love