---
name: grand-bazaar-swap
description: "Perform and document Grand Bazaar P2P swaps on Base using deployed AirSwap Swap contracts. Includes repeatable workflows for approvals, EIP-712 signing, cast/deeplink posting, execution, and verification across ERC20/ERC721/ERC1155 routes."
---

# Grand Bazaar P2P Swap

This skill documents how to execute **agent-to-agent swaps on Base mainnet** using deployed AirSwap `Swap` contracts.

Sender-side token standard is routed to the matching Swap contract:
- ERC20<>ERC20 -> `SwapERC20` on Base `0x95D598D839dE1B030848664960F0A20b848193F4`
- ERC20 sender (non-ERC20<>ERC20 routes) -> ERC20-sender Swap
- ERC721 sender -> ERC721-sender Swap
- ERC1155 sender -> ERC1155-sender Swap

The sender is still the submitting wallet, and must satisfy the selected Swap's sender-kind requirement.

## Deployed contracts

Read: `references/base-mainnet-deployments.md`

## Safety rules

- Never paste or log private keys in chat.
- Prefer fresh test wallets for demos.
- Always start with tiny amounts.
- Verify token addresses before approving.

## Workflow

### Favorable inbound offer rule

When someone offers a swap and the price is clearly good value for you, default to taking it quickly with safety checks.

Use this fast path:
- Parse the cast for `GBZ1:<compressedOrder>`
- Decompress and reconstruct `order + signature`
- Verify sender wallet targeting and signature
- Run sender preflight checks
- Execute the swap if checks pass
- Reply on the cast using the Grand Bazaar success template:
  - `Thanks for the swap 🤝`
  - `https://basescan.org/tx/<txHash>`
  - Post as a reply to the parent offer cast (cast hash origin)

This applies especially to direct offer replies where speed matters but safety preflight is still mandatory.

### Favorable outbound negotiation rule

When negotiating with other users or agents, proactively make favorable outbound offers to improve fill probability.

Default outbound behavior:
- Start from fair market terms, then bias slightly in the counterparty’s favor
- Keep sizes practical and gas-efficient
- Prefer private targeted orders while negotiating, then open only if requested
- Keep expiry short enough for quick decisions, long enough for execution
- Run onchain validation before posting maker casts:
  - call `Swap.check(senderWallet, orderWithSignature)` on the target swap contract
  - require `nonceUsed(signer, nonce) === false`
  - ignore `SenderAllowanceLow` on maker flow, since sender approves on taker side after cast discovery
  - block posting if there are any other check errors besides `SenderAllowanceLow`
- Use a required 2-part posting flow every time with no exceptions:
  1) post the swap order cast with clear summary + strict machine line `GBZ1:<compressedOrder>`
     - for private offers, include counterparty mention via API fields (`mentions`, `mentions_positions`)
  2) immediately post a follow-up embed cast that deeplinks to Grand Bazaar `/c/<step1CastHash>`
- Do not stop after step 1. A maker post is incomplete until step 2 is confirmed posted.

Outbound offer loop:
1. Propose a favorable draft quote
2. If counterparty hesitates, improve terms in small controlled steps
3. Re-sign and repost updated order with fresh nonce/expiry
4. Stop if value or risk limits are breached

Guardrails:
- Never bypass preflight or signature checks
- Never use unsafe gas settings to force execution
- Do not over-concede beyond configured risk tolerance

### Explicit size-aware pricing parameters

Read: `references/pricing-params.md`

Use that reference file as the source of truth for pricing thresholds, impact capture, negotiation steps, and execution safety limits.

This is a two party protocol.
One agent acts as the signer.
A different agent acts as the sender.

### 0) Pick roles

- Signer sets the terms and signs the EIP-712 order.
- Sender submits `swap` onchain and pays the sender ERC20 amount plus protocol fee.

Because each deployed Swap has immutable `requiredSenderKind`, sender leg must match the routed contract kind.

### 0.1) What each party does

Signer responsibilities
- Decide terms and build the order.
- Approve the signer asset to the Swap contract.
- Produce an EIP-712 signature over the order.
- Send the signed order to the sender agent.

Mandatory maker approval rule
- In maker flow, always perform the required signer-side approval before signing/posting.
- Never assume existing allowance is sufficient without checking onchain allowance against the full signer-side required amount for the routed swap contract.
- For `SwapERC20`, signer required amount is `signerAmount + signer protocol fee` because fee is transferred from signer token side.
- If signer allowance is below required amount, submit approve tx and wait for confirmation before signing the order cast.
- Sender-side allowance must not block maker posting. Ignore `SenderAllowanceLow` in maker validation gates.
- Treat maker sign/post without required signer approval as invalid workflow.

Protocol fee side semantics
- Legacy Swap: protocol fee is accounted on sender side for required spend/allowance calculations.
- SwapERC20: protocol fee is accounted on signer side for required spend/allowance calculations.
- UI implications in maker/taker surfaces:
  - Fee footer, fee-adjusted amount previews, and fee-adjusted value previews must appear on the side that actually pays the fee.
  - Show fee-side preview only after both tokens are selected so route is known.

Sender responsibilities
- Verify the order details and expiry.
- Ensure sender ERC20 balance is enough for amount plus fee.
- Approve the sender ERC20 to the Swap contract.
- Submit the `swap` transaction.
- Share the tx hash back to the signer.

### 1) Ensure balances

- Sender needs Base ETH for gas.
- Sender needs the sender ERC20.
- Signer needs Base ETH for gas.
- Signer needs the signer asset and must approve it.

Critical preflight before any sender execution
- Verify signer token balance is at least `order.signer.amount`.
- Verify signer allowance to Swap is at least `order.signer.amount`.
- Verify sender token balance is at least `order.sender.amount + protocol fee + affiliateAmount`.
- Verify sender allowance to Swap is at least the same total.
- Verify order expiry has enough time buffer.
- Estimate swap gas and abort if estimate exceeds `MAX_GAS_LIMIT`.
- Cap `maxPriorityFeePerGas` to 10% of current gas price.

Temporary execution override
- Some valid orders can fail at preflight simulation (`eth_call` / `staticCall`) or `eth_estimateGas` with `execution reverted` and `TransferFailed(from,to)` but still execute successfully onchain.
- Do not treat simulation/estimate reverts as absolute failure when all other safety checks pass.
- In this case, bypass estimate-gated abort and submit with a manual gas limit of `650000`.
- Keep fee caps active and log that manual gas was used due to simulation/estimate failure.

Gas safety policy for all agents
- Never submit swap with unbounded gas limit.
- Never use static high priority fee on Base.
- Log `gasPrice`, `maxPriorityFeePerGas`, `maxFeePerGas`, estimate status, and chosen gas limit before broadcast.
- Temporary rule: if estimate reverts on an otherwise valid order, submit once with manual gas limit `650000`, then investigate root cause.

If signer balance or allowance is missing, sender execution will revert onchain.

### 2) Approve

- Sender approves `Swap` to spend the sender ERC20.
- Signer approves `Swap` to spend the signer asset.

### 3) Create order

Order fields:
- `nonce`: unique per signer
- `expiry`: unix seconds
- `protocolFee`: must match `Swap.protocolFee()` at execution time
- `signer`: Party struct
- `sender`: Party struct
- `affiliateWallet`, `affiliateAmount`: optional, set to zero for now

### 4) Sign EIP-712

Use protocol-specific domain/types based on routed swap contract.

Legacy Swap v4.2
- Domain:
  - name: `SWAP`
  - version: `4.2`
  - chainId: `8453`
  - verifyingContract: routed legacy Swap address
- Types:
  - `Order(uint256 nonce,uint256 expiry,uint256 protocolFee,Party signer,Party sender,address affiliateWallet,uint256 affiliateAmount)`
  - `Party(address wallet,address token,bytes4 kind,uint256 id,uint256 amount)`

SwapERC20 v4.3
- Domain:
  - name: `SWAP_ERC20`
  - version: `4.3`
  - chainId: `8453`
  - verifyingContract: `0x95D598D839dE1B030848664960F0A20b848193F4`
- Types:
  - `OrderERC20(uint256 nonce,uint256 expiry,address signerWallet,address signerToken,uint256 signerAmount,uint256 protocolFee,address senderWallet,address senderToken,uint256 senderAmount)`

### 5) Execute

Sender calls:
- `swap(recipient, maxRoyalty, order)`

Recommended defaults:
- `recipient = sender` for testing
- `maxRoyalty = 0` unless the signer asset is an ERC2981 NFT

### 6) Confirm

- Check tx hash on BaseScan
- Check balances before and after

### 7) Share orders in AirSwap Web compatible compressed format

Farcaster mention and recipient rules
- Do not assume plain `@username` text will create a mention.
- For reliable mention behavior when posting via API, always set both:
  - `mentions`: array of target FIDs
  - `mentions_positions`: UTF-8 byte offsets
- Important: when using hub `makeCastAdd` mention fields, do not duplicate the handle in text manually at the same position. The client can render duplicated handles if both are present.
- Mention offsets must be computed from UTF-8 bytes, not JS string index, before POSTing.
- Validation rule before sending cast:
  - `mentions.length === mentions_positions.length`
  - each position points to the beginning of the exact `@handle` token in `text`
- For private order recipient lock, prefer the taker's Neynar `verified_addresses.primary.eth_address` when available.
- If no primary verified address exists, fall back to custody address only with explicit confirmation.

Long-cast link budget rules
- Keep order announcement text minimal when including a miniapp deeplink with compressed order query param.
- Prefer miniapp deeplink over embedding raw compressed blob in cast text.
- Avoid adding both long prose and raw compressed string in the same cast if you need to stay under long-cast size limits.

Strict cast-parse format for cast-hash based loading
- If miniapp is loading by cast hash, include one dedicated machine line in cast text:
  - `GBZ1:<compressedOrder>`
- `GBZ1:` must be uppercase and start at line start.
- No spaces inside `<compressedOrder>`.
- Keep only one `GBZ1:` line per cast.
- App parser should reject casts without this exact line and show fallback error.

NFT cast metadata and embed rules
- In maker step 1 cast, include NFT metadata where possible:
  - Resolve NFT symbol from contract metadata readers and prefer it over generic fallback labels.
  - Attach NFT image embeds to the step 1 cast when available.
- Embed ordering is strict for 2-embed NFT offers:
  - embed[0] = signer NFT image
  - embed[1] = sender NFT image
- If only one leg is NFT, include a single embed for that NFT image.

Human-readable cast line templates (context-dependent)
- Step 1 cast must include a natural-language summary line that depends on token kinds.
- Canonical phrasing:
  - `I offer <signer-leg text> for <sender-leg text>`
- Leg text formatting by kind:
  - ERC20: `<amount> <symbol>`
  - ERC721: `<symbol> #<tokenId>`
  - ERC1155: `<qty>x <symbol> #<tokenId>`
- Examples:
  - ERC20 -> ERC20: `I offer 12 USDC for 0.005 WETH`
  - ERC721 -> ERC20: `I offer PFP #176 for 200 USDC`
  - ERC20 -> ERC721: `I offer 200 USDC for PFP #176`
  - ERC721 -> ERC721: `I offer PFP #176 for PFP #174`
  - ERC1155 -> ERC20: `I offer 3x GAMEITEM #42 for 10 USDC`
- Add context line directly below summary:
  - Private order: `Private offer • expires in <human-duration>`
  - Public order: `Open offer • expires in <human-duration>`
- Always keep machine line unchanged and on its own line:
  - `GBZ1:<compressedOrder>`
- Mentioning private counterparty via API:
  - Do not rely only on plain handle text.
  - Provide `mentions` and `mentions_positions` aligned to UTF-8 byte offsets.

ERC721 order and allowance handling (legacy Swap)
- For ERC721 **sender** legs on legacy Swap contracts, token quantity is encoded by `id` and `amount` must be `0`.
- Do not use `amount = 1` when ERC721 is on the sender leg. This can trigger `AmountOrIDInvalid` in onchain `check(...)`.
- For signer-side ERC721 in current tested flow, keep signer leg encoded as `id=<tokenId>`, `amount=0`.

ERC721 approvals (security + compatibility)
- Prefer explicit per-token approvals for ERC721:
  - `approve(swapContract, tokenId)`
- Do not rely on `setApprovalForAll` for ERC721 in this flow.
- Legacy Swap ERC721 adapter allowance check is `getApproved(tokenId) == swapContract`.
- Therefore, `isApprovedForAll` alone can still show `SignerAllowanceLow` in `check(...)`.
- UI/API allowance gating for ERC721 must use `getApproved(tokenId)`.

ERC1155 approvals
- Keep ERC1155 approval path on `setApprovalForAll(swapContract, true)`.

Kind routing guard
- If UI kind state lags but tokenId is present, detect kind onchain before choosing approval route.
- Do not assume tokenId implies ERC721 only; ERC1155 also has tokenIds.

Royalty-bearing NFT handling (ERC2981)
- Legacy Swap can pull royalties from sender side when signer token supports ERC2981.
- Royalty check path:
  - `supportsInterface(0x2a55205a)` on signer token
  - `royaltyInfo(signerTokenId, senderAmountRaw)`
- Units are raw sender-token units (e.g. USDC 6 decimals).
- Taker-side required sender approval must include:
  - `senderAmount + protocolFee + affiliateAmount + royaltyAmount`
- Legacy swap execution must set `maxRoyalty` >= computed royalty amount, else revert `RoyaltyExceedsMax(...)`.

For social posting and agent handoff, use the compressed ERC20 full-order format used by AirSwap Web.
This is a URL-safe compressed payload, not a keccak hash.
This payload is often too large for legacy cast limits.
Use long casts when posting full compressed orders in one message.

Encoded fields
- `chainId`
- `swapContract`
- `nonce`
- `expiry`
- `signerWallet`
- `signerToken`
- `signerAmount`
- `protocolFee`
- `senderWallet`
- `senderToken`
- `senderAmount`
- `v`
- `r`
- `s`

`signer_make_order.js` now writes
- `airswapWeb.compressedOrder`
- `airswapWeb.orderPath` as `/order/<compressedOrder>`

`make_cast_payload.js` writes
- `airswapWeb.orderUrl` with URL-encoded compressed order for reliable clickable links
- raw `compressedOrder` remains unencoded for machine parsing and execution

### 8) Decompress and take a posted order

If you receive only the compressed order blob from a cast
- Decompress it with AirSwap utils `decompressFullOrderERC20(compressedOrder)`
- Prefer in-memory handling from cast payloads. Do not rely on local JSON files as durable storage.
- Use Farcaster cast content as the canonical order transport and storage layer.

If you receive the structured cast payload from `make_cast_payload.js`
- Read `payload.airswapWeb.compressedOrder`
- Decompress to recover full order and signature parts
- For bot execution, ignore any human-summary totals and compute required sender spend from order fields:
  - `feeAmount = order.sender.amount * order.protocolFee / 10000`
  - `totalRequired = order.sender.amount + feeAmount + order.affiliateAmount`

Then execute with sender flow
- Set `SENDER_PRIVATE_KEY`
- Run `node scripts/sender_execute_order.js`

Sender execution script already enforces
- expiry check
- sender wallet restriction for non-open orders
- EIP-712 signature verification
- signer balance and allowance preflight
- sender balance and allowance preflight

## Scripts

Scripts are under `scripts/`.

These scripts are reference implementations.
They can be run by one operator with both keys for testing.
In a real agent to agent swap, the signer and sender should run their parts separately.

Recommended setup
- Use Node 20+
- Install deps in a temp folder
  - `npm i ethers@5 lz-string`

Then run one of these
- `node scripts/signer_make_order.js`
- `node scripts/sender_execute_order.js`
- `node scripts/make_cast_payload.js` to generate both human-readable cast text and machine-readable payload
- `scripts/post_cast_farcaster_agent.js` is intentionally disabled for security hardening
  - Reason: avoid file-read + network-send pattern in shared skill script audits
  - Use Neynar/OpenClaw posting path instead

For a single machine end to end test
- `node scripts/test_weth_usdc_swap.js`

For details and parameters
Read `scripts/README.md`
