Agent Commerce Protocol (ACP)

View Source

raxol_acp is an Elixir/OTP implementation of the Virtuals ACP for selling agent services on Base. Where raxol_payments is about paying (an agent that buys things), raxol_acp is about being paid (an agent that offers a service and accepts on-chain settlement).

Status: pre-alpha. Not yet on Hex; use the path dep at packages/raxol_acp/.

Job Lifecycle

Every job is a state machine. One supervised Raxol.ACP.JobSession runs per active job, registered by {chain_id, job_id}:

:open -> :budget_set -> :funded -> :submitted -> :completed
                                            \-> :rejected
(any non-terminal) -> :expired

Raxol.ACP.JobSession.Status is a pure module holding the status enum and the legal transition graph. JobSession is the GenServer: it tracks role-aware status, keeps a chronological entry log, notifies subscribers, and emits [:raxol, :acp, :job_session, :transition] telemetry on every change. apply_event/3 applies an OBSERVED status (an on-chain or SSE event) directly, bypassing role and adjacency gating.

{:ok, _pid} =
  Raxol.ACP.JobSession.Supervisor.start_session(
    chain_id: 8453,
    job_id: "job-42",
    role: :provider
  )

# Provider actions transition the session; the Provider driver (below)
# pairs each with the matching hook write on-chain.
{:ok, :budget_set} = Raxol.ACP.JobSession.set_budget({8453, "job-42"}, budget)
{:ok, :submitted} = Raxol.ACP.JobSession.submit({8453, "job-42"}, deliverable)

The seller-side glue is Raxol.ACP.JobSession.Provider: for each lifecycle step it invokes the offering Handler (via JobSession.HandlerSeam), writes the hook call on-chain through HookClient + ProviderAdapter (the commit point -- a failed write leaves the session untouched), then mirrors the resulting status with apply_event/3.

Offerings

An offering is a service the agent sells. Declared via the Offering DSL:

defmodule Raxol.ACP.Offerings.SentimentAnalysis do
  use Raxol.ACP.Offering,
    name: "Sentiment Analysis",
    price_usdc: 10,
    sla_minutes: 5,
    cluster: "analytics"

  # Decide whether to take the job. Return {:accept, response} to accept
  # and set the budget, or {:reject, reason} to bow out.
  @impl true
  def handle_request(request, _ctx) do
    {:accept, %{quoted_usdc: 10, text: request.text}}
  end

  # Payment is escrowed; produce the deliverable.
  @impl true
  def handle_deliver(request, _ctx) do
    {:deliver, %{sentiment: analyze(request.text)}}
  end
end

The DSL injects the Handler behaviour and registers metadata in the ETS-backed Registry. JobSession.Provider invokes the handler at each lifecycle step and writes the matching hook call on-chain with the configured wallet.

Xochi Cross-Chain Transfer (the first offering)

Raxol.ACP.Xochi.TransferOffering is the first offering: a pure storefront for cross-chain stablecoin transfers. raxol never signs or holds the buyer's funds.

  • The buyer quotes and signs a Xochi intent themselves (Raxol.Payments.Protocols.Xochi.quote_and_sign/3) and puts the signed bundle in the job requirement's signed_intent.
  • On delivery, Raxol.ACP.Xochi.Settler relays that bundle to Xochi via execute_signed/2 (no re-signing) and polls it to settlement; the deliverable is the on-chain settlement tx hashes.
  • The transfer settles through Xochi off-escrow, so the ACP core's take never bites it. The job is a plain job (hook = address(0)); the ACP budget is only raxol's storefront fee -- 8 bps of the transfer, set via :fee_bps. On completion the provider nets budget * 0.90.

packages/raxol_acp/examples/buyer_signed_intent.exs shows the buyer flow and the requirement/deliverable schemas to register on the marketplace.

On-Chain Writes

The v2 model writes hook calls to the active AgenticCommerceV3 core; there is no separate memo model (the v1 createMemo / Raxol.ACP.ContractClient write surface and the :acp_version switch were retired -- see MIGRATION_V2.md). Raxol.ACP.HookClient exposes set_budget / submit / complete / reject, each dispatched through an injected Raxol.ACP.ProviderAdapter:

  • SCA: sponsored ERC-4337 v0.7 UserOps via Raxol.ACP.Wallet.SCA (Alchemy Modular Account v2 + paymaster). Self-deploys the account on the first write.
  • JSONRPC: a plain EOA signing EIP-1559 typed transactions (Raxol.ACP.Onchain.{RPC, Transaction, RLP}), with nonce assignment serialized through Raxol.ACP.Wallet.NonceServer.
  • Mock: in-process, for tests and mix raxol_acp.bench.

Raxol.ACP.ABI hand-rolls the Solidity encoder for the ACP methods. Real ABIs are vendored under priv/abi/; verified Base addresses live in Raxol.ACP.Chain (the active acp_core_address plus the hook/router/subscription addresses; the legacy acp_contract_address/acp_router_address remain only for indexer back-compat).

Expiry

JobSession reaches the terminal :expired status via expire/2 from any non-terminal status, so a job whose counterparty abandoned it can be closed rather than left wedged. :expired, :completed, and :rejected are terminal -- the session process stops with :normal once it reaches one. On-chain escrow handling on expiry is the AgenticCommerceV3 core's responsibility.

Nonce Serialization

The Raxol.ACP.Wallet.NonceServer GenServer serializes EVM nonce assignment through its mailbox, so two concurrent EOA writes from one wallet can never sign the same nonce. ProviderAdapter.JSONRPC routes every send through it. A send that fails before broadcast resyncs the counter, so the next send re-fetches the pending nonce from chain and re-fills the gap rather than leaving a hole that would strand every later transaction. The SCA/UserOp path uses ERC-4337 EntryPoint nonces and is unaffected.

Seller Stack

Opt-in via :seller_enabled in config:

  • Backend.InMemory: in-process request queue (default)
  • Queue: bounded mailbox, backpressure
  • Runtime: worker pool dispatching to handlers
  • Supervisor: ties it all together

Backend.WebSocket (Socket.IO v4 / Engine.IO over Mint.WebSocket, talking to Virtuals' relayer) is implemented alongside Backend.InMemory; the Queue drives JobSession.Provider. The protocol spec is available via the virtuals-protocol-acp skill.

Status

Pre-alpha, not yet on Hex. The Wallet.SCA ERC-4337 stack, the real vendored ABIs (priv/abi/), and the Backend.WebSocket Socket.IO client are implemented; the on-chain paths are fork-validated against the real deployed core on Base. mix raxol_acp.bench runs end-to-end against the InMemory backend.

See Also