Xochi private execution protocol.
Xochi is the default agent-facing protocol for cross-chain transfers. It routes intents through the Xochi dark pool where Riddler (and other solvers) compete to fill them. This is the cash-positive path with tier-based fees.
Unlike x402/MPP, Xochi is not a 402-triggered protocol. It uses an explicit quote -> sign -> execute -> poll flow.
Usage
config = %{base_url: "https://api.xochi.fi", auth: {:member, "..."}}
wallet = MyWallet
{:ok, quote} = Xochi.quote(config, %QuoteRequest{...})
{:ok, exec} = Xochi.execute(config, quote, wallet)
{:ok, status} = Xochi.poll_status(config, exec.intent_id)Fee Tiers
The fee is layered: a solver spread plus gas floor (never discounted), a venue fee, and a routing fee. Trust discounts carve the venue and routing layers only, so the solver floor is identical at every tier. Headline totals by tier and asset:
| Tier | Score | Stable | Volatile |
|---|---|---|---|
| Standard | 0-24 | 0.22% | 0.40% |
| Trusted | 25-49 | 0.19% | 0.35% |
| Verified | 50-74 | 0.15% | 0.29% |
| Premium | 75-99 | 0.12% | 0.25% |
| Institutional | 100+ | 0.10% | 0.22% |
A quote will carry an optional fee_breakdown with the per-layer split (solver,
venue, routing) once the worker emits it; QuoteResponse does not parse it yet.
Origin-pull solver allowlist
The origin pull authorizes the solver to collect funds from the agent wallet. The pull recipient/spender is the solver's collection address; by default it is not pinned (solver addresses rotate and there is no client-facing manifest). An operator who knows their solver set can pin it:
config :raxol_payments, :pull_solver_allowlist, ["0xsolver...", "0xsolver2..."]When set, a pull whose to (ERC-3009) or spender (Permit2) is not in the list
is rejected before any signature. When unset (the default), the address is not
bound. See GitHub #333.
Summary
Types
A buyer's pre-signed Xochi intent bundle, as handed to the storefront relay.
Functions
Raise when the origin-pull solver pin is fail-open for the given allowlist and
requirement flag, otherwise return :ok. Call at boot in a fund-moving
deployment so a missing solver pin halts startup instead of silently signing
ERC-3009 pulls to an unverified recipient. See GitHub #333.
Fetch a deposit-route quote and verify its deposit_attestation before
returning the deposit instructions -- the authenticated form of a Tron-origin
quote.
Sign and execute an intent from a quote.
Like execute/3, but binds the served pull_authorization to the caller's
intended transfer (request) before signing it.
Relay a buyer's pre-signed intent to Xochi WITHOUT re-signing.
Request a cross-chain intent quote from Xochi.
True when the origin-pull solver pin would let an unverified recipient through:
an empty allowlist with the pin not required. In that state validate_pull
accepts any ERC-3009 to (Permit2 stays fail-closed regardless). The boot-time
assert_origin_pull_pinned!/2 uses this to refuse a fail-open prod start.
Poll intent status until terminal (completed/failed/expired) or timeout.
Like poll_status/3 but also returns the elapsed milliseconds to the terminal
status, so the caller can report whether settlement landed within budget.
Buyer-side one-shot: get_quote/2 then sign_intent/3.
Sign a quoted intent into a relayable bundle WITHOUT executing it.
Like sign_intent/2, but binds the served pull_authorization to the caller's
intended transfer (request) before signing it -- see execute/4 for why this
matters. Pass nil only when there is no pull authorization to validate; a
pull presented with a nil request fails closed.
Full transfer flow: quote -> sign -> execute -> poll.
Validate the served origin-pull authorization against the intended transfer
WITHOUT signing or executing -- the read-only counterpart of the check
execute/4 runs before it signs.
Verify a deposit-route quote's attestation against the pinned signer and return
the deposit instructions, or a fail-closed error. See deposit_route_quote/3
for signer resolution.
Types
A buyer's pre-signed Xochi intent bundle, as handed to the storefront relay.
Keys may be atoms (internal callers) or strings (decoded from an ACP
requirement). Required: intent_id, quote_id, signature, nonce.
Optional: pull_signature (nil for non-pulling methods), aztec_proof
(shielded claims).
Functions
Raise when the origin-pull solver pin is fail-open for the given allowlist and
requirement flag, otherwise return :ok. Call at boot in a fund-moving
deployment so a missing solver pin halts startup instead of silently signing
ERC-3009 pulls to an unverified recipient. See GitHub #333.
@spec deposit_route_quote( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.DepositRouteRequest.t(), keyword() ) :: {:ok, map()} | {:error, term()}
Fetch a deposit-route quote and verify its deposit_attestation before
returning the deposit instructions -- the authenticated form of a Tron-origin
quote.
A non-EVM origin has no gasless pull, so the quote returns a bare
deposit_address the payer must fund directly. A MITM or compromised endpoint
could swap that address, so raxol verifies the attestation recovers to the
pinned signer BEFORE surfacing the address, failing closed when no signer is
pinned or the attestation does not verify. raxol never sends the funds; the
returned instructions are for the caller's own Tron wallet to fund, then poll
with poll_status/3.
Signer resolution, in precedence order: opts[:deposit_attestation_signer]
(an operator's out-of-band pin), else config :raxol_payments, :xochi_deposit_attestation_signer, else the live capability matrix's
deposit_attestation_signer.
Options
:deposit_attestation_signer-- pin the expected signer explicitly.:capabilities-- a pre-fetchedCapabilities.t()(skips the network).
@spec execute( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), module() ) :: {:ok, Raxol.Payments.Xochi.Schemas.ExecuteResponse.t()} | {:error, term()}
Sign and execute an intent from a quote.
Signs the EIP-712 typed data from the quote response using the wallet, then submits the signed intent for execution.
@spec execute( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), module(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t() | nil ) :: {:ok, Raxol.Payments.Xochi.Schemas.ExecuteResponse.t()} | {:error, term()}
Like execute/3, but binds the served pull_authorization to the caller's
intended transfer (request) before signing it.
The agent signs an ERC-3009/Permit2 authorization the solver serves; a hostile
or compromised quote endpoint could otherwise serve one that pulls the wallet's
full balance to an attacker. With the request, the origin pull is checked
against the intended signer, token, chain, and amount before any signature is
released. Pass nil only when there is no pull authorization to validate; a
pull authorization presented with a nil request fails closed.
@spec execute_signed(Raxol.Payments.Xochi.Client.config(), signed_intent()) :: {:ok, Raxol.Payments.Xochi.Schemas.ExecuteResponse.t()} | {:error, term()}
Relay a buyer's pre-signed intent to Xochi WITHOUT re-signing.
The storefront (pure-relay) primitive. The buyer quoted and signed the EIP-712
intent (and any origin-pull authorization) against Xochi itself, then handed
raxol the opaque bundle {intent_id, quote_id, signature, nonce, pull_signature}. raxol posts it verbatim; Riddler verifies the signature
against its own server-persisted quote, so neither raxol nor the buyer can
forge the amount or route.
Unlike execute/3,4, this takes no wallet and releases no signature -- raxol
is never on the fund-signing path. Fails closed with
{:error, {:invalid_signed_intent, field}} on a missing or malformed field,
before any network call.
@spec get_quote( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t() ) :: {:ok, Raxol.Payments.Xochi.Schemas.QuoteResponse.t()} | {:error, term()}
Request a cross-chain intent quote from Xochi.
True when the origin-pull solver pin would let an unverified recipient through:
an empty allowlist with the pin not required. In that state validate_pull
accepts any ERC-3009 to (Permit2 stays fail-closed regardless). The boot-time
assert_origin_pull_pinned!/2 uses this to refuse a fail-open prod start.
@spec poll_status(Raxol.Payments.Xochi.Client.config(), String.t(), keyword()) :: {:ok, Raxol.Payments.Xochi.Schemas.IntentStatus.t()} | {:error, term()}
Poll intent status until terminal (completed/failed/expired) or timeout.
Fast-polls inside the settlement budget window, then backs off. See
Raxol.Payments.Poll for the timing options (:budget_ms,
:fast_interval_ms, :slow_interval_ms, :timeout_ms).
@spec poll_status_timed(Raxol.Payments.Xochi.Client.config(), String.t(), keyword()) :: {:ok, Raxol.Payments.Xochi.Schemas.IntentStatus.t(), non_neg_integer()} | {:error, term()}
Like poll_status/3 but also returns the elapsed milliseconds to the terminal
status, so the caller can report whether settlement landed within budget.
@spec quote_and_sign( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t(), module() ) :: {:ok, signed_intent()} | {:error, term()}
Buyer-side one-shot: get_quote/2 then sign_intent/3.
Fetches a quote for request and signs it into a relayable bundle. The buyer
hands the bundle to a storefront (e.g. as an ACP requirement's signed_intent)
which relays it via execute_signed/2; the storefront never re-signs.
@spec sign_intent(Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), module()) :: {:ok, signed_intent()} | {:error, term()}
Sign a quoted intent into a relayable bundle WITHOUT executing it.
The buyer-side counterpart to execute_signed/2: validates the quote, signs
the EIP-712 intent with wallet, and returns the opaque bundle
%{intent_id, quote_id, signature, nonce} (plus pull_signature when the
quote carried an origin-pull authorization) to hand to a storefront/relay or
to execute_signed/2 directly. Does not talk to the worker.
@spec sign_intent( Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), module(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t() | nil ) :: {:ok, signed_intent()} | {:error, term()}
Like sign_intent/2, but binds the served pull_authorization to the caller's
intended transfer (request) before signing it -- see execute/4 for why this
matters. Pass nil only when there is no pull authorization to validate; a
pull presented with a nil request fails closed.
@spec transfer( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t(), module(), keyword() ) :: {:ok, Raxol.Payments.Xochi.Schemas.IntentStatus.t()} | {:error, term()}
Full transfer flow: quote -> sign -> execute -> poll.
Convenience function that runs the complete Xochi intent lifecycle. Returns the final terminal status.
@spec validate_pull( Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), Raxol.Payments.Xochi.Schemas.QuoteRequest.t(), module() ) :: :ok | {:error, term()}
Validate the served origin-pull authorization against the intended transfer
WITHOUT signing or executing -- the read-only counterpart of the check
execute/4 runs before it signs.
Returns :ok when the quote's pull_authorization binds to request (signer,
token, chain, value, envelope type, expiry) and its to/spender satisfies the
configured solver pin, or {:error, {:authorization_mismatch, field}} otherwise.
A quote with no pull authorization is :ok -- there is nothing to pull. Lets a
preflight reject a forged or rotated-solver quote across every corridor before
any funded run, with no funds moved.
@spec verify_deposit_route( Raxol.Payments.Xochi.Client.config(), Raxol.Payments.Xochi.Schemas.DepositRouteRequest.t(), Raxol.Payments.Xochi.Schemas.QuoteResponse.t(), keyword() ) :: {:ok, map()} | {:error, term()}
Verify a deposit-route quote's attestation against the pinned signer and return
the deposit instructions, or a fail-closed error. See deposit_route_quote/3
for signer resolution.