Skip to content

Add off-escrow proportional-fee job type#31

Open
DROOdotFOO wants to merge 5 commits into
Virtual-Protocol:mainfrom
DROOdotFOO:feat/off-escrow-proportional-fee
Open

Add off-escrow proportional-fee job type#31
DROOdotFOO wants to merge 5 commits into
Virtual-Protocol:mainfrom
DROOdotFOO:feat/off-escrow-proportional-fee

Conversation

@DROOdotFOO

@DROOdotFOO DROOdotFOO commented Jul 5, 2026

Copy link
Copy Markdown

What this is

A facilitator wants to sell cross-chain stablecoin transfers on ACP and charge a fee that scales with transfer size (say, 8 bps on the notional). Right now that offering isn't listable. priceType: "percentage" is tied to requiredFunds: true, so the only way to price proportionally is to take custody of the buyer's capital and settle it same-chain on Base. For an off-escrow cross-chain relay neither of those holds, which leaves a flat fee, and a flat fee can't scale with size. That's the blocker. The full write-up is in DROOdotFOO/raxol#373 (spec) and #371 (storefront variants).

The reference use case is Raxol's Xochi.TransferOffering: the buyer signs their own intent (ERC-3009 / Permit2), the principal moves off-escrow straight from their wallet to the destination chain, and ACP only ever escrows the fee.

Worth knowing before you review

I went in expecting to relax a percentage-vs-custody check somewhere in the SDK. It isn't here. This SDK never computes a fee. setBudget is always caller-supplied, and priceType/priceValue are metadata it never reads. The coupling that blocks us lives in the registry/backend. On-chain, AgenticCommerceV3 has no requiredFunds or percentage concept at all: a plain job (hook = address(0)) with budget = fee already completes and splits the fee 90/5/5. We fork-traced that path end to end.

So this PR can't flip the switch on its own. The backend still has to accept percentage + requiredFunds:false at offering-creation time. What it does is make the client side of the model first-class and give you a concrete artifact to mirror instead of a prose spec. If you'd rather the fee helper or settlement convention live somewhere else, that's fine; the shapes matter more than the file names.

What's in it

All additive. No change to existing offerings, hooks, or the createJobFromOffering branching. The off-escrow path is the plain-job branch that already existed.

  • AcpAgentOffering.feeBasisField (optional): names the requirement field that carries the fee notional, e.g. notionalAtomic. Plus PRICE_TYPE constants.
  • core/fee.ts: computePercentageFee, readFeeBasis, assertNotionalMatches. bigint math, rounds down.
  • core/settlement.ts: buildSettlementDeliverable / parseSettlementDeliverable and a JSON schema, so the deliverable is a settlement tx hash the evaluator can verify.
  • src/examples/off-escrow-percentage/: a runnable buyer/seller pair. The Xochi quote/sign/settle calls are stubbed (they live in raxol, which is Elixir) and marked TODO. The ACP job lifecycle is real.
  • README sections and index exports.

The two custody concerns

Both came up in review, and both are handled in code rather than in the description.

Faked notional: the buyer declares the notional, but it's also bound in their signed intent. The seller calls assertNotionalMatches(declared, boundInIntent) and rejects a mismatch before it sets a budget. The buyer independently re-derives the fee from the notional and the rate on budget.set, and funds only if it equals what the seller proposed. Both sides check, and the buyer sees the exact fee before paying.

Disputes: because ACP only holds the fee, a dispute reduces to one question, did the settlement tx land on the destination chain. Yes, release the fee. No, refund it. The principal never sat in anyone's wallet, so the worst case is a fee refund, not lost capital. That's easier to reconcile than the custodial percentage flow, where the buyer's whole principal passes through the seller.

One thing to confirm before merge

Is priceValue on a percentage offering basis points or percent? Our agent prices in bps; I suspect the frontend shows percent. I didn't want to bake in a guess, so computePercentageFee takes the unit explicitly ("bps" | "percent"), and the example flips a single FEE_UNIT constant. Pin the backend convention and that's settled. It's the only open question on the SDK side.

Manual testing

  • tsc --noEmit on the library: clean.
  • Full typecheck including the new example: clean. The pre-existing llm/ example still needs @anthropic-ai/sdk; untouched here.
  • Helper checks pass: 8 bps of 1000 USDC = 0.80, fractional bps, percent mode, round-down, settlement round-trip, and a notional mismatch throwing.
  • Job lifecycle (create, setBudget(fee), fund(fee), submit(settlement), complete) runs against baseSepolia with the two example scripts and real wallets. The Xochi relay is stubbed, so no cross-chain funds move.

Refs DROOdotFOO/raxol#373, DROOdotFOO/raxol#371


Note

Low Risk
Additive SDK helpers, types, docs, and an example; no changes to job creation hooks or existing offering flows.

Overview
Introduces off-escrow proportional-fee facilitator jobs: principal settles outside ACP (e.g. signed intent), while escrow holds only a fee derived from a requirement notional (priceType: "percentage", requiredFunds: false). Existing createJobFromOffering behavior is unchanged; this path uses the existing plain job branch (requiredFunds: false).

Public API: optional AcpAgentOffering.feeBasisField, PRICE_TYPE / PriceType, and exports from core/fee.ts (computePercentageFee, readFeeBasis, assertNotionalMatches) and core/settlement.ts (buildSettlementDeliverable, parseSettlementDeliverable, schema).

Example: src/examples/off-escrow-percentage/ documents seller notional checks, buyer fee preview before fund(), and settlement-hash deliverables (Xochi relay stubbed). README and examples index updated accordingly.

Reviewed by Cursor Bugbot for commit cc70c37. Bugbot is set up for automated code reviews on this repo. Configure here.

Enable facilitator offerings that charge a proportional fee while the
principal settles OUTSIDE ACP escrow (e.g. a cross-chain stablecoin
transfer via the buyer's signed intent). ACP escrows only the fee.

Additive and fenced-off (priceType "percentage" + requiredFunds:false +
settlement_tx_hash deliverable) - it reuses the existing plain-job path
(hook = address(0)); no contract change and no impact on existing
offerings.

- types: optional AcpAgentOffering.feeBasisField + PRICE_TYPE consts
- core/fee.ts: computePercentageFee, readFeeBasis, assertNotionalMatches
- core/settlement.ts: build/parseSettlementDeliverable + schema
- example: off-escrow-percentage/ (stubbed Xochi relay), README sections

Fee unit (priceValue bps vs percent) is passed explicitly and never
guessed - the backend convention must be confirmed before merge.

Refs DROOdotFOO/raxol#373, DROOdotFOO/raxol#371
Comment thread src/examples/off-escrow-percentage/seller.ts
Comment thread src/examples/off-escrow-percentage/buyer.ts
The settlement tx is mined on the transfer's destination chain
(toChainId), not the ACP job chain (Base).

- seller: recover the requirement from the job entries at job.funded and
  stamp the deliverable with toChainId (was session chainId)
- buyer: reject a settlement proof whose chainId isn't the destination
  before completing

Both flagged by Cursor Bugbot on Virtual-Protocol#31.
@DROOdotFOO

Copy link
Copy Markdown
Author

Worth noting flagged bugs were both in the example-files, not the core helpers (fee.ts/settlement.ts) lol.

Review pass on the off-escrow proportional-fee change:

- settlement: reject non-positive / non-integer chainId in
  parseSettlementDeliverable; schema chainId is integer >= 1
- make the trust boundary explicit: the stub notional check reads a
  plaintext field and is not a real check until the intent signature is
  verified; note the on-chain tx-existence check the evaluator must do,
  and that skip-evaluation auto-releases the fee
- note the fee is in the notional token's units (example uses USDC)
- rewrite comments to drop shouty caps and PR/review meta-narration
@DROOdotFOO

DROOdotFOO commented Jul 5, 2026

Copy link
Copy Markdown
Author

Self-review pass, two follow-up commits (b5a6300, f8f7ba0).

  • Cursor's two flags fixed: the settlement deliverable now stamps the transfer destination chain, and the buyer checks the proof's chain before completing.
  • Trust boundary made explicit in code + README: the stub notional check reads a plaintext field and proves nothing until the intent signature is verified (that decode is Xochi-specific, lives in raxol). Likewise the evaluator must confirm the settlement tx exists on the destination chain; the example checks shape + chain only.
  • Needs an evaluator: under skip-evaluation the fee auto-releases on submit with no settlement check.
  • parseSettlementDeliverable now rejects NaN/negative/non-integer chainId.

No change to core helper behavior or existing flows.

One open question before merge: is a percentage priceValue bps or percent? The helper takes the unit explicitly, so it's a one-line change once you confirm.

Comment thread src/examples/off-escrow-percentage/buyer.ts Outdated
Comment thread src/examples/off-escrow-percentage/seller.ts
Second Bugbot pass on f8f7ba0:

- buyer: derive expectedDestChainId from the requirement actually sent,
  not the sample constant, so a customized toChainId stays consistent
- seller: on job.funded, if the requirement can't be recovered, notify
  the room instead of returning silently (job expires, fee refunds)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a73dc37. Configure here.

Comment thread src/core/fee.ts
Comment thread src/examples/off-escrow-percentage/buyer.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant