Arcdesk

Docs

The complete reference for trading on, making on, and integrating with Arcdesk. The desk is a non-custodial tool: these documents plus the contract source are the entire trust model — there is no fine print anywhere else.

Network: loading…

Networks · Order lifecycle · Taker guide · Maker guide · Fees & timeouts · Contract reference · REST API · Arc specifics · Security & trust · Security review · FAQ

Networks & contracts

Addresses below are read live from the desk API, so they are always the pair this deployment actually settles on. Verify independently before sending funds: call usdc() and operator() on each escrow and check they match this page.

LegChainChain IDContract
Payment
Liquidity
USDC (payment side)
USDC (Arc)Arc0x3600000000000000000000000000000000000000

Both legs run the same bytecode (ArcdeskEscrow.sol). Nothing configures a contract into a role; the chain it sits on decides which half of the interface gets used. One audit therefore covers both deployments.

Order lifecycle

quoted ──▶ reserved ──▶ paid ──▶ maker_paid ──▶ delivered
                │           │
                │           └── payment deadline passes ──▶ refunded   (your money back)
                └── reservation deadline passes ──▶ liquidity returns to the offer
StateOn-chain meaningWho acted
quotedOrder priced; nothing on-chain yetAPI
reservedreserve() confirmed on Arc: maker liquidity is locked to your address under a hashlock, for ≥90 minDesk keeper
paidlockPayment() confirmed on the payment chain: your funds escrowed under the same hashlock, ≤30 minYou (your only transaction)
maker_paidclaimPayment() confirmed: maker took the payment, and doing so published the secret on-chainMaker
deliveredclaimReservation() confirmed on Arc: your USDC left escrow to your addressAnyone (the desk submits it for you)
refundedrefundPayment() after the deadline: full escrow returned to youAnyone (button on Orders, or cast)

Taker guide — buying USDC on Arc

  1. Pick an offer on the Trade page. The price you compare is the premium; the 2% desk fee is the same everywhere.
  2. Enter amount + your Arc address. Any address you control on Arc; it does not need gas, history, or a balance.
  3. Get quote & reserve. Before you pay anything, the desk locks your USDC out of the maker's offer, addressed to you. You can verify this yourself: read reservations(orderId) on the Arc escrow and check recipient is your address and deadline is comfortably after your payment deadline.
  4. Approve + pay — two wallet transactions on the payment chain. The second escrows proceeds + fee under the hashlock. This is the last thing you ever have to do.
  5. Wait ~a minute. The maker claims your payment (publishing the secret) and the desk delivers your USDC on Arc. You pay no Arc gas at any point.

If nothing is delivered: your money is not gone, it is locked with a countdown. After the payment deadline (max 30 minutes) the Refund button on Orders returns it — or from any wallet: refundPayment(orderId, payer) on the payment escrow. The desk cannot block this.

Maker guide — selling USDC on Arc

You escrow USDC on Arc behind an offer priced at your premium. When a buyer's payment is locked on the payment chain, claiming that payment is what releases your escrowed USDC to them — you can never lose the USDC without simultaneously receiving the payment, and the buyer can never take your USDC without you being paid.

ActionCallNotes
Post an offerapprove() + postOffer(offerId, amount, premiumBps, expiry)offerId: any unused 32-byte id (hash something unique). premiumBps: 800 = 8%. expiry: unix timestamp, or 0 for open-ended.
Top upfundOffer(offerId, amount)Adds liquidity to a live offer.
Close & withdrawcancelOffer(offerId)Instant for unreserved liquidity. Reserved slices come back automatically when their reservations expire unclaimed.
Sweep an expired offerexpireOffer(offerId)Permissionless housekeeping for time-limited offers; open-ended offers only close via cancel.
# post 100 USDC at 8%, open-ended, from your Arc wallet
cast send $ARC_USDC "approve(address,uint256)" $ARC_ESCROW 100000000 \
  --rpc-url $ARC_RPC --private-key $KEY --legacy
cast send $ARC_ESCROW "postOffer(bytes32,uint256,uint16,uint64)" \
  $(cast keccak "my-offer-$(date +%s)") 100000000 800 0 \
  --rpc-url $ARC_RPC --private-key $KEY --legacy

Maker accounts pay Arc gas (which is USDC), so keep a little slack beyond your offer size. Today the settlement secret is generated by the desk when it quotes an order against your offer; a maker API where you bring your own hashlock (so the desk never holds the secret) is the next milestone and will be required for third-party makers on mainnet.

Fees, premiums, timeouts

ParameterValueWhere enforced
Desk fee2%Quoted into lockPayment; paid by the taker to the treasury at settlement
Maker premium0 – 655.35% (uint16 bps)Set per offer; the market decides what clears
Payment window≤ 30 min — MAX_PAYMENT_WINDOWContract rejects longer locks; after the deadline refundPayment is open to anyone
Reservation window≥ 90 min — MIN_RESERVATION_WINDOWContract rejects shorter reservations, so delivery always outlives payment by ≥1h
Offer expirymaker's choice or 0 (open-ended)Expired offers stop reserving instantly; sweep is permissionless

Quote math for buying A USDC at premium p bps: proceeds = A × (10000 + p) / 10000, fee = proceeds × feeBps / 10000, you pay proceeds + fee. All amounts are 6-decimal USDC integers; division truncates.

Contract reference — ArcdeskEscrow

Solidity ^0.8.24, OpenZeppelin SafeERC20 + ReentrancyGuard. All state-changing functions are nonReentrant. No proxy, no upgradability, no pause on refund paths.

Liquidity leg (Arc)

FunctionAccessReverts with
postOffer(bytes32,uint256,uint16,uint64)anyoneoffer exists · zero amount · bad expiry
fundOffer(bytes32,uint256)makernot maker · inactive · zero amount
cancelOffer(bytes32)makernot maker
expireOffer(bytes32)anyoneno offer · open-ended · not expired · inactive
reserve(bytes32,bytes32,address,uint256,bytes32,uint64)operatornot operator · order exists · bad recipient · bad hashlock · reservation too short · inactive · offer expired · insufficient
claimReservation(bytes32,bytes32)anyonenot reserved · bad preimage
refundReservation(bytes32)anyonenot reserved · not yet

Payment leg (source chain)

FunctionAccessReverts with
lockPayment(bytes32,address,address,uint256,uint256,bytes32,uint64)anyone (the taker)order exists · bad payee · bad hashlock · deadline passed · payment window too long
claimPayment(bytes32,address,bytes32)anyone with the preimagenot locked · bad preimage
refundPayment(bytes32,address)anyonenot locked · not yet

Admin surface (deliberately small)

FunctionAccessBlast radius
setOperator(address)ownerChanges who may reserve. Cannot touch escrowed funds or refund paths.
transferOwnership(address)ownerHands over the above. Nothing else.

Events

OfferPosted(offerId, maker, amount, premiumBps, expiry)
OfferFunded(offerId, amount)          OfferCancelled(offerId, returned)
OfferExpired(offerId, returned)
Reserved(orderId, offerId, recipient, amount, hashlock, deadline)
ReservationClaimed(orderId, preimage) ReservationRefunded(orderId, amount)
PaymentLocked(orderId, payer, maker, proceeds, fee, hashlock, deadline)
PaymentClaimed(orderId, preimage)     PaymentRefunded(orderId)
OperatorChanged(operator)             OwnerChanged(owner)

Hashlock scheme: hashlock = keccak256(abi.encodePacked(secret)) where secret is 32 bytes generated by the maker side. Revealing it in either claim* makes it public for the other chain — that is the entire cross-chain binding; there is no bridge, oracle, or messaging layer to trust.

REST API

Base URL: the origin serving this page (configurable under Profile → Settings). JSON in/out. Amounts are 6-decimal integer strings. No authentication — the API can only do things that are safe for anyone to do; your funds are guarded by the contracts, not by this server.

EndpointReturns
GET /health{ ok, keeper, sourceEscrow, arcEscrow, feeBps }
GET /offers{ offers: [{ offerId, maker, remaining, premiumBps, expiry }] } — expiry 0 = open-ended
GET /stats{ trades, volume6, fees6 }
POST /ordersReserves on Arc first, then returns exact lockPayment args (below)
GET /orders/:idOrder row incl. status and settlement tx hashes
GET /address/:addr/ordersHistory for a taker address (latest 100)
POST /orders
{ "offerId": "0x…", "amount6": "10000000",
  "takerSource": "0xYourPaymentChainAddr", "recipientArc": "0xYourArcAddr" }

200 →
{ "orderId": "0x…", "escrow": "0x…", "usdc": "0x…",
  "lockPayment": { "orderId", "maker", "treasury", "proceeds", "fee", "hashlock", "deadline" },
  "totalPay": "…", "payDeadline": …, "resvDeadline": … }

errors: 404 offer not found · 409 not enough liquidity · 410 offer expired · 400 bad address

Integration contract: send exactly one approve(escrow, proceeds+fee) then lockPayment(…) with the returned args, then poll GET /orders/:id until delivered. If you see refunded or your deadline passes, call refundPayment(orderId, payer) yourself — never resend a payment for the same orderId (the contract rejects duplicates).

Arc-specific behavior

Security & trust model

PartyCan doCannot do
Desk / keeperStall a trade (not reserve, not deliver)Take escrowed funds. Reserved liquidity only pays the pre-committed recipient; payments only pay the maker + treasury named at lock time; refunds are permissionless.
MakerDecline to claim (trade times out, you get refunded)Take your payment without releasing your USDC — claiming publishes the secret that unlocks your delivery, and the contract forces the delivery window to outlive the payment window by ≥1h.
Contract ownerSwap the operatorTouch funds, block refunds, upgrade code (there is no proxy).
Circle (Arc)Blocklist any Arc address, including the escrow— (external, disclosed above)

Audit status: unaudited. 29 automated tests (unit + fork) and live settlement runs back the current deployment, and mainnet will launch behind per-trade and total-value caps that are raised gradually — but unaudited means unaudited. Do not put in more than you can lose, and read the ~350 lines of ArcdeskEscrow.sol yourself; it is short on purpose.

Security review (adversarial)

The system was tested from an attacker's seat, on contracts and the live API. Findings and their status:

FindingSeverityStatus
Order-id front-run: lock dust under a public orderId to block the real taker (DoS)MediumFIXED — payments keyed by (orderId, payer); slots are independent
Keeper claims any hashlock-matching payment, so a forged lock could reveal the secret earlyHighFIXED — keeper only claims the exact quoted payment (payer, maker, amounts)
Unauthenticated POST /orders reserves maker liquidity for free (griefing)High (practical)MITIGATED — per-offer cap of 3 unpaid reservations, per-IP rate limit, auto-refund sweeper at the reservation deadline
Raw SQLite error string returned on bad inputLow (info leak)FIXED — strict input validation, clean errors
Operator can drain any maker's offer (compromised/rogue keeper)High (design)KNOWN — inherent keeper trust for makers; the fix is maker-signed reservations (EIP-712), a prerequisite before third-party makers on mainnet. Today the desk is the only maker, so no third-party funds are at risk.

Attacks the design already refuses: redirecting a delivery or payment, refunding to a non-payer, forging or replaying a claim, refunding a reservation early, seizing admin, and the owner touching escrowed funds — all covered by the automated suite.

FAQ

Why does USDC cost more than $1 here?
Arc is a closed network: official bridges are shut, so USDC already inside trades at a premium set by makers. When Circle opens the door, premiums collapse — this market exists exactly as long as it stays closed, and we say so openly.

Do I need anything on Arc first?
No. An empty address you control is enough. You never send an Arc transaction; delivery comes to you.

What if the site disappears mid-trade?
Your payment sits in a contract with a ≤30-minute deadline. After it passes, refundPayment(orderId, payer) from any wallet returns everything. The orderId is on your Orders page and in the PaymentLocked event your own wallet emitted.

Can I trade the other direction (Arc → out)?
The contract already supports it (the legs are symmetric); the desk UI ships buy-side first. Sell-side opens when there is maker demand for it.

Is there an official token, points, or airdrop?
No. Anyone claiming otherwise in our name is scamming you.