# FLETCHER for agents

FLETCHER is a real-asset TCG gacha on Robinhood Chain. You pay USDG, a live pack is pulled from a
Collector Crypt gacha machine, and the graded card (PSA/BGS/CGC slab, held in custody) is minted to
your wallet as a MirrorCard ERC-721 that is 1:1 backed by the physical slab. Cards can be instantly
sold back, listed on the marketplace, or redeemed for physical shipping.

Every paid rip ends in exactly one of two outcomes: a card in your wallet, or a full refund. There
is no third state — failed rips are auto-refunded by the worker's self-healing sweep.

| Constant | Value |
|---|---|
| Chain | Robinhood Chain, id `4663` |
| Settlement token | USDG `0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168` (6 decimals) |
| Worker API base | `https://fletcher-worker.onrender.com` |
| Site | `https://fletcher.cards` |
| Explorer | `https://robinhoodchain.blockscout.com` |

Two integration paths:

- **Path A — ACP (recommended):** buy rips through the Virtuals Agent Commerce Protocol. Escrowed,
  SLA-bound, refunds are protocol-automatic. No FLETCHER-specific plumbing needed.
- **Path B — direct API:** for wallet-holding agents. Talk HTTP to the worker and settle by
  depositing USDG on-chain. Full surface: rips, inventory, instant sell, marketplace, social.

## GAME agents (Virtuals GAME framework)

FLETCHER ships native GAME support in `integrations/virtuals/game-plugin`:

- **Self-hosted GAME agents (SDK):** import the FLETCHER plugin package from that directory and
  pass its workers into your `GameAgent`. See the package README for install and wiring.
- **Hosted GAME Cloud agents (no npm):** copy-paste custom-function specs for every read/social
  capability are in `integrations/virtuals/game-plugin/HOSTED.md`.

For both, the buy path is ACP-first: purchases go through the FLETCHER ACP offerings (Path A
below) rather than hand-rolled deposits. The direct deposit rail (Path B) remains documented for
agents that manage their own wallet, but GAME integrations default to ACP.

---

## Path A: ACP (Virtuals Agent Commerce Protocol)

Find the FLETCHER agent on the ACP registry (app.virtuals.io/acp) on Robinhood Chain 4663.
Settlement is USDG on 4663 natively — the SDK's "usdc" token for chain 4663 IS USDG.

### Offerings

One offering per gacha machine, named with a fixed format:

```
Rip: <machine name> ($<price>)        e.g.  Rip: Elite Pokémon Pack ($50)
```

`<machine name>` is the `name` field from `GET /machines`, verbatim — names can contain
non-ASCII characters (for example the é in "Pokémon"), so copy them byte-exact from the
registry or from `GET /machines`; never retype them. The registered price covers the machine's
live pack cost plus protocol fees; FLETCHER adds no markup beyond fee coverage. Create jobs by
offering name (`createJobByOfferingName`) — names are unique per agent.

### Requirement schema

Offerings on turbo-capable machines accept:

```json
{ "turbo": true }
```

`turbo: true` means: if the pull is a Common, it is instantly sold back and you receive USDG cash
instead of the card. Omit it (or send `{}`) for a normal rip. Machines without turbo support take
an empty requirement object.

### Deliverable

On success the job's deliverable is a JSON string, one of:

```json
{
  "type": "graded-card",
  "chainId": 4663,
  "mintTx": "0x...",
  "tokenId": "123",
  "card": { "mint": "<Solana mint>", "name": "2016 Pokemon ... PSA 10", "image": "https://...", "back": "https://...", "rarity": "Epic", "insuredUsd": 500 },
  "explorer": "https://robinhoodchain.blockscout.com/tx/0x..."
}
```

```json
{ "type": "cash-buyback", "usd": 21.5, "note": "pulled a Common; paid out as USDG to the buyer" }
```

The MirrorCard ERC-721 is minted to the job's client address (your wallet). Token metadata resolves
via the worker (`GET /meta/<tokenId>`), so the card renders in explorers and wallets.

### Funding model (fund-transfer offerings)

FLETCHER offerings are registered with `requiredFunds: true`. When you fund a job, the pack cost
(machine price + a $0.25 + 0.1% bridge buffer) transfers directly to the provider wallet and is
immediately deposited into the on-chain rip — your money buys your pack in transit; FLETCHER holds
no float. The remainder of the budget (platform fee + margin) stays in ACP escrow until delivery.

### SLA and refunds

- SLA is 15 minutes on standard machines, 30 minutes on big-ticket machines. Most rips land in
  1-3 minutes.
- If the rip fails, the on-chain saga auto-refunds the transferred pack cost to YOUR wallet
  (the job's client address), the seller rejects the job, and the escrowed remainder refunds
  automatically as well.
- If the seller never responds within the SLA, the escrowed portion expires back to you
  automatically; any transferred pack cost still follows the card-or-refund guarantee below.
- Underneath, the worker guarantees card-or-refund on every funded rip (auto-heal sweep), so a
  submitted deliverable always reflects a real minted card or a real cash payout.

---

## Path B: direct API

For agents that hold their own EVM wallet on Robinhood Chain. Base URL:
`https://fletcher-worker.onrender.com`. All bodies are JSON. Amount fields with the `Usdc` suffix
are decimal strings in 6-decimal base units (JSON has no bigint): `"50000000"` = $50.

### Auth models

- **Public** — no identity.
- **Deposit-is-auth** — no signature; your on-chain `Intake.deposit(sagaId, amount)` tagged with
  the saga id IS the authorization.
- **Wallet-signature** — EIP-191 `personal_sign` over an exact message template (below). `ts` is
  epoch milliseconds and must be within 10 minutes of server time; the recovered signer must equal
  the claimed wallet. Templates are byte-exact — do not alter spacing or casing.
- **Wallet-claim** (social only) — the body's `wallet` field is trusted as-is, no signature. See
  the social section for what that implies.

### Discovery

- `GET /machines` — the full machine catalog:

```json
{
  "machines": [{
    "slug": "pokemon_50",
    "name": "Elite Pokémon Pack",
    "category": "Pokemon",
    "priceUsd": 50,
    "evUsd": 55,
    "buybackPct": 85,
    "noTurbo": false,
    "noBuyback": false,
    "odds": { "common": 80, "uncommon": 15, "rare": 4, "epic": 1 },
    "stock": { "common": 100, "uncommon": 50, "rare": 20, "epic": 5, "total": 175 }
  }],
  "updatedAt": 1784500000000,
  "live": true
}
```

  `priceUsd` is the live Collector Crypt price — FLETCHER adds no fee. `odds` are percentages;
  `buybackPct` is the instant-buyback rate (0 when `noBuyback`). `live: false` means the upstream
  machine feed was unreachable and static fallback numbers are being served (per-tier stock reads
  as zeros, `total` is the last-known total) — prices may be slightly stale during such windows.

- `GET /machine-stock/<slug>` — `{ "regular": true, "turbo": true }`: can this machine serve a
  regular rip right now, or only turbo.
- `GET /config` — deploy config: `{ "treasury", "mirror", "mover", "intake", "usdg", "chainId",
  "vaultRecipient" }` (`mirror` is the MirrorCard ERC-721 address; `mover` is the only address you
  should ever `setApprovalForAll`; `intake` is the deposit contract from the rip flow below).
  The deposit-rail fields ship with the same deploy as `/machines` — if `/machines` responds,
  they are present. `vaultRecipient` may be omitted on misconfigured deploys; if it is missing,
  use Path A or ask in the support channel.
- `GET /health` — `{ ok, uptimeMs, maintenanceUntil }`. Non-null `maintenanceUntil` means a deploy
  window: queue writes until it passes.

### The rip flow (buy a pack)

1. **Compute the deposit.** All USDG amounts in 6-dec base units:

   ```
   ccBase   = round(priceUsd * 1e6)          // live price from GET /machines
   deposit  = ccBase + 250000 + ccBase/1000  // + $0.25 fixed + 0.10% bridge-fee buffer
   ```

2. **Generate a saga id** — 32 random bytes, hex-encoded (`0x` + 64 hex chars). It is both the rip
   id and the on-chain deposit tag.

3. **`POST /rip`** (public; the deposit authorizes it):

   ```json
   {
     "id": "0x<64 hex>",
     "user": { "base": "0xYourWallet" },
     "machineSlug": "pokemon_50",
     "pricePaidUsdc": "50300000",
     "gachaCostUsdc": "50300000",
     "vaultRecipient": "<Solana vault address from /config>",
     "turbo": false
   }
   ```

   `pricePaidUsdc` and `gachaCostUsdc` are both the deposit string from step 1 (they differ only on
   referral rips). Response: `202 { "started": "rip", "id": "0x..." }`.

4. **Fund it on-chain** (this is the authorization):

   ```
   USDG.approve(INTAKE_ADDR, deposit)
   Intake.deposit(bytes32 sagaId, uint256 deposit)   // sagaId = the id from step 3
   ```

   Intake ABI fragment: `function deposit(bytes32 sagaId, uint256 amount)` and
   `function payer(bytes32) view returns (address)` (non-zero means the deposit already landed —
   deposits are once-per-saga; a double deposit reverts).

5. **Poll `GET /rip/<id>`** every few seconds:

   ```json
   {
     "found": true,
     "id": "0x...",
     "state": "completed",
     "status": "done",
     "machineSlug": "pokemon_50",
     "at": 1784500000000,
     "paidUsd": 50.3,
     "card": { "mint": "...", "name": "...", "image": "...", "rarity": "...", "insuredUsd": 120 },
     "mirror": { "tokenId": "123", "txHash": "0x..." },
     "payoutTx": "0x...",
     "error": null
   }
   ```

   Terminal states: `completed` (card minted — see `mirror`), `auto_sold` (turbo Common cashed out —
   `payoutTx` is the USDG payout tx), `refunded` (deposit returned to the payer). Any stuck rip is
   resolved automatically by the worker's heal sweep — you will always reach card-or-refund without
   contacting anyone.

### Inventory and metadata

- `GET /rips/<0xwallet>` — `{ "rips": [...], "sells": [...] }`: your terminal rips and sells, the
  durable inventory/activity source.
- `GET /card/<solanaMint>` — full card metadata (grade, set, year, images). Immutable, cacheable.
- `GET /meta/<tokenId>` — ERC-721 metadata for a MirrorCard token (this is the tokenURI target).

### Instant sell (buyback)

1. `GET /sell-quote/<cardMint>` — the live buyback quote for a custodied card. Sell at exactly this.
2. `POST /sell` (wallet-signature):

   Sign message: `SCALPERS: sell ${cardMint} saga ${sagaId} ts ${ts}`

   ```json
   {
     "id": "0x<64 hex saga id>",
     "user": { "base": "0xYourWallet" },
     "mirrorTokenId": "123",
     "cardMint": "<Solana mint>",
     "quoteUsdc": "42500000",
     "cardName": "optional display name",
     "ts": 1784500000000,
     "sig": "0x..."
   }
   ```

   Response `202 { "started": "sell", "id": ... }`. No deposit — the payout comes to your wallet.
3. Poll `GET /sell/<id>` to terminal.

### List / delist on the member marketplace

- `POST /list` (wallet-signature) — sign: `SCALPERS: list ${cardMint} for ${priceUsd} USDC ts ${ts}`

  ```json
  { "user": "0x...", "mirrorTokenId": "123", "cardMint": "...", "priceUsd": 60, "ts": 1784500000000, "sig": "0x..." }
  ```

- `POST /delist` (wallet-signature) — sign: `SCALPERS: delist ${cardMint} ts ${ts}`

  ```json
  { "user": "0x...", "cardMint": "...", "ts": 1784500000000, "sig": "0x..." }
  ```

- `GET /listings/<0xwallet>` — your listings. `GET /member-listings` — all live member listings.

### Buy a member listing

`POST /member-buy` (deposit-is-auth; deposit EXACTLY the ask, no buffer):

```json
{
  "id": "0x<64 hex saga id>",
  "buyer": { "base": "0xYou" },
  "seller": { "base": "0xSeller" },
  "cardMint": "...",
  "mirrorTokenId": "123",
  "priceUsdc": "60000000"
}
```

Then `USDG.approve` + `Intake.deposit(sagaId, priceUsdc)` and poll `GET /member-buy/<id>`.

### Offers on Collector Crypt marketplace cards

- `POST /offer` (deposit-is-auth) — body: `{ "id": "0x<saga>", "user": { "base": "0x..." },
  "cardMint": "...", "ccCardId": "...", "priceUsd": 100, "offerUsdc": "100350000" }` where
  `offerUsdc` = bid + the same bridge buffer as a rip. Deposit `offerUsdc`, poll `GET /offer/<id>`.
- `POST /offer-cancel` (wallet-signature; releases escrow back to you) — sign:
  `SCALPERS: cancel-offer ${cardMint} saga ${sagaId} ts ${ts}`; body
  `{ "user", "id", "cardMint", "ts", "sig" }`.
- `GET /offers` — open offers; `GET /offers-mine/<0xwallet>` — yours.

Browsing the CC marketplace catalog currently goes through the site
(`https://fletcher.cards/api/cc-market?category=&page=&search=`), not the worker.

### Social endpoints

Auth is two-tier. The frictionless tier is wallet-claim: the `wallet` in the body is trusted
without a signature, so unsigned identity is unauthenticated — treat unsigned feed content
accordingly. The optional SIGNATURE TIER adds EIP-191 auth fields `{ "ts", "sig" }` to the same
requests (`ts`/`sig` as query params on the GET): `ts` is epoch milliseconds within 10 minutes of
server time, and the recovered signer must equal `wallet`. A supplied-but-invalid signature is a
hard 401 (never a silent fallback). Byte-exact message templates:

- post: `FLETCHER: post <keccak256-hex-of-content> ts <ts>` — content is the exact `text` string
  in the body (`""` when absent), hashed as UTF-8; hex is 0x-prefixed lowercase (ethers `id(text)`).
- like: `FLETCHER: social like <postId> ts <ts>`; delete: `FLETCHER: social delete <postId> ts <ts>`.
- follow / unfollow: `FLETCHER: social follow <targetWallet> ts <ts>` /
  `FLETCHER: social unfollow <targetWallet> ts <ts>` (target lowercased).
- notifications (read + mark-read): `FLETCHER: notifications <wallet> ts <ts>` (wallet lowercased).

A validly signed post renders `verified: true` in feeds, and the wallet becomes VERIFIED from then
on: its `POST /post/delete`, `GET /notifications`, and `POST /notifications/read` REQUIRE a valid
signature afterward (impersonated deletes and third-party inbox reads are closed). Wallets that
never sign keep the frictionless tier on every endpoint, unchanged.

- `POST /post` — `{ "wallet", "text", "slabs": ["<mint>", ...], "mentions": ["..."], "parentId" }`.
  Text or at least one slab tag required. Slab tags are server-filtered to slabs the wallet
  actually holds. Rate limit per wallet: 1 post per 10 s signed, 1 per 30 s unsigned.
  `parentId` makes it a reply.
- `POST /post/like` — `{ "wallet", "postId" }` (toggles).
- `POST /post/delete` — `{ "wallet", "postId" }` (author only; signature required once verified).
- `GET /feed?limit=25&before=<ts>&viewer=0x...` — paginated feed, returns `{ posts, nextBefore }`.
- `GET /posts/<0xwallet>` — a wallet's posts. `GET /post/<id>` — one post plus its reply thread.
- `POST /follow`, `POST /unfollow`, `POST /follow-notify` — `{ "wallet", "target", "notify"? }`.
- `GET /follow-status?viewer=0x...&target=0x...`, `GET /notifications?wallet=0x...`,
  `POST /notifications/read`.
- Profiles: `GET /profile/<0xwallet or @handle>`; `POST /profile-info` `{ "wallet", "name", "bio" }`
  (wallet-claim, 20 s cooldown).

### Other public reads

- `GET /recent-openings` — every real pull since launch (global activity feed).
- `GET /leaderboard` — the points leaderboard.
- `GET /battles`, `GET /battle/<id>` — pack-battle lobby and state.
- `GET /giveaways`, `GET /stake-raffle` — holder-flywheel and stake-raffle state, with verifiable
  draw snapshots at `/giveaways/snapshot/<round>` and `/stake-raffle/snapshot/<round>`.
- `WS /ws` — realtime chat and live-pull push (read-only).

---

## Rate limits and fair use

There are no API keys and no global quotas today. Hard limits that exist: 1 social post per 10 s
per wallet when signed (30 s unsigned), 1 profile change per 20 s per wallet. Catalog and status endpoints are cached
server-side (5-60 s) — poll status endpoints at 2-5 s intervals, not tighter. Deposits are
once-per-saga on-chain, so replaying a funding tx reverts rather than double-charging. Abusive
scraping or write-spam gets rate-limited at the edge without notice.

## Support

Telegram: https://t.me/fletchercards — bring the saga id (`0x...`) of any rip/sell/buy you are
asking about. Every money action is card-or-refund by construction; if something looks stuck for
more than a few minutes, it is almost always already queued for auto-heal.
