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.
| Leg | Chain | Chain ID | Contract |
|---|---|---|---|
| Payment | — | — | — |
| Liquidity | — | — | — |
| USDC (payment side) | — | — | — |
| USDC (Arc) | Arc | — | 0x3600000000000000000000000000000000000000 |
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
| State | On-chain meaning | Who acted |
|---|---|---|
| quoted | Order priced; nothing on-chain yet | API |
| reserved | reserve() confirmed on Arc: maker liquidity is locked to your address under a hashlock, for ≥90 min | Desk keeper |
| paid | lockPayment() confirmed on the payment chain: your funds escrowed under the same hashlock, ≤30 min | You (your only transaction) |
| maker_paid | claimPayment() confirmed: maker took the payment, and doing so published the secret on-chain | Maker |
| delivered | claimReservation() confirmed on Arc: your USDC left escrow to your address | Anyone (the desk submits it for you) |
| refunded | refundPayment() after the deadline: full escrow returned to you | Anyone (button on Orders, or cast) |
Taker guide — buying USDC on Arc
- Pick an offer on the Trade page. The price you compare is the premium; the 2% desk fee is the same everywhere.
- Enter amount + your Arc address. Any address you control on Arc; it does not need gas, history, or a balance.
- 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 checkrecipientis your address anddeadlineis comfortably after your payment deadline. - Approve + pay — two wallet transactions on the payment chain. The second escrows
proceeds + feeunder the hashlock. This is the last thing you ever have to do. - 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.
| Action | Call | Notes |
|---|---|---|
| Post an offer | approve() + 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 up | fundOffer(offerId, amount) | Adds liquidity to a live offer. |
| Close & withdraw | cancelOffer(offerId) | Instant for unreserved liquidity. Reserved slices come back automatically when their reservations expire unclaimed. |
| Sweep an expired offer | expireOffer(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
| Parameter | Value | Where enforced |
|---|---|---|
| Desk fee | 2% | Quoted into lockPayment; paid by the taker to the treasury at settlement |
| Maker premium | 0 – 655.35% (uint16 bps) | Set per offer; the market decides what clears |
| Payment window | ≤ 30 min — MAX_PAYMENT_WINDOW | Contract rejects longer locks; after the deadline refundPayment is open to anyone |
| Reservation window | ≥ 90 min — MIN_RESERVATION_WINDOW | Contract rejects shorter reservations, so delivery always outlives payment by ≥1h |
| Offer expiry | maker'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)
| Function | Access | Reverts with |
|---|---|---|
| postOffer(bytes32,uint256,uint16,uint64) | anyone | offer exists · zero amount · bad expiry |
| fundOffer(bytes32,uint256) | maker | not maker · inactive · zero amount |
| cancelOffer(bytes32) | maker | not maker |
| expireOffer(bytes32) | anyone | no offer · open-ended · not expired · inactive |
| reserve(bytes32,bytes32,address,uint256,bytes32,uint64) | operator | not operator · order exists · bad recipient · bad hashlock · reservation too short · inactive · offer expired · insufficient |
| claimReservation(bytes32,bytes32) | anyone | not reserved · bad preimage |
| refundReservation(bytes32) | anyone | not reserved · not yet |
Payment leg (source chain)
| Function | Access | Reverts 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 preimage | not locked · bad preimage |
| refundPayment(bytes32,address) | anyone | not locked · not yet |
Admin surface (deliberately small)
| Function | Access | Blast radius |
|---|---|---|
| setOperator(address) | owner | Changes who may reserve. Cannot touch escrowed funds or refund paths. |
| transferOwnership(address) | owner | Hands 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.
| Endpoint | Returns |
|---|---|
| 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 /orders | Reserves on Arc first, then returns exact lockPayment args (below) |
| GET /orders/:id | Order row incl. status and settlement tx hashes |
| GET /address/:addr/orders | History 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
- USDC is the gas token. The ERC-20 view at
0x3600…0000is the account's native balance ÷ 10¹² (6-decimal surface over an 18-decimal core). One pool of funds, two interfaces. - Transfers run through chain precompiles (
0x1800…0000balances,0x1800…0001compliance). This is why fresh Arc accounts can receive but not send — and why the desk delivers to you instead of asking you to claim. - Circle operates a blocklist at the chain level. A blocklisted address cannot move USDC, and that includes escrow contracts. This is an external risk the desk cannot remove; it is disclosed here because you should size positions knowing it exists.
- Timestamps can stall on Arc when blocks are sparse. The contract windows are generous (90 min floor) partly for this reason.
Security & trust model
| Party | Can do | Cannot do |
|---|---|---|
| Desk / keeper | Stall 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. |
| Maker | Decline 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 owner | Swap the operator | Touch 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:
| Finding | Severity | Status |
|---|---|---|
| Order-id front-run: lock dust under a public orderId to block the real taker (DoS) | Medium | FIXED — payments keyed by (orderId, payer); slots are independent |
| Keeper claims any hashlock-matching payment, so a forged lock could reveal the secret early | High | FIXED — 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 input | Low (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.