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:
Locks liquidity from a maker’s deposit on the Escrow contract
Creates a commitment to send fiat payment off-chain
Records all trade parameters on-chain for later verification
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
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
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 ;
}
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 Allowance
Use Different Addresses
// 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:
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.
Obtain payment proof
Get a zkTLS attestation or proof from the attestation service proving your payment.
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
Error Cause Solution InsufficientDepositLiquidityDeposit doesn’t have enough available funds Choose a different deposit or reduce amount AmountBelowMinAmount less than deposit’s minimum Increase your intent amount AmountAboveMaxAmount exceeds deposit’s maximum Reduce your intent amount RateBelowMinimumConversion rate too low Increase conversion rate to match deposit AccountHasActiveIntentYou already have an active intent Cancel existing intent or wait for it to complete InvalidSignatureGating signature is invalid or expired Request 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