Grasshopper — GH
Voucher vendor adapter — Octopus's own inverted-flow claim system. Octopus mints code+PIN and ships only SHA-256 hashes to a Cloudflare Worker.
Quick facts
Code GH · Line voucher · Auth X-Auth-Token header · Model inverted — Octopus mints code+PIN, sends only hashes · Webhook none (claim-callback) · Delivery plaintext code+PIN + claim URL · Balance n/a (no prepaid float) · Adapter services/external_vendors/vouchers/grasshopper_vendor.go; admin push http/handler/admin_grasshopper.go; downstream frontend/grasshopper/
Grasshopper is Octopus's own downstream claim system (a Cloudflare Worker + D1). Unlike every other vendor — where Octopus buys a code the vendor mints — for GH, Octopus mints the code+PIN itself and ships only SHA-256 hashes to GH. Redemption happens on claim.octopuscards.io, which calls back into the Octopus topup/eSIM API. See Frontends and Products & Catalog.
The inverted mint-and-hash model (headline)
CreateOrder does not ask GH for a code. It:
- Mints locally —
GenerateVoucherCode()(xxxx-xxx-xxx) +GenerateSecurePINString()(6-digit crypto PIN). - Hashes —
codeHash = HashSHA256(code),pinHash = HashSHA256(pin). - Sets expiry Octopus-side — now + 1 year.
- POSTs only the hashes —
POST /api/vouchers {code_hash, pin_hash, expires_at, voucher_type_id, denomination}. The plaintext never leaves Octopus. - Returns the plaintext in the
Voucherfor delivery to the buyer — the only place the raw code exists.
GH's D1 stores only code_hash/pin_hash. Octopus is the sole holder of plaintext, transiently at create time. So GetOrderStatus/GetVouchers look up by voucher id, never by code. Expiry is owned by Octopus, not GH.
Auth
Per-request X-Auth-Token: <apiKey> (no login). Auth/RedoAuth just Ping (GET /api/status, unauthenticated). Config from the GH vendor row's host + api_key attributes.
Claim callback — GH → Octopus (inverted flow)
The redemption UI + callback live in the Cloudflare Worker (frontend/grasshopper/src/api/claim.ts), not Go:
- User enters the code on
claim.octopuscards.io→ worker hashes it, looks up bycode_hashin D1, checks expiry. - User enters PIN (+ inputs); worker validates
pin_hash, then calls back:POST {OCTOPUS_API_URL}/api/v1/topups/orders(or/esim/orders) with a JWT andredeem_voucher_code= the plaintext code recovered client-side. - The worker persists
topup_order_id/topup_status/topup_client_refon the D1 row and polls; reconciliation byclient_referenceavoids double-charging.
So money moves user claim → GH Worker → Octopus topups API → real vendor. GH itself holds no value (GetBalance returns empty; ProductAvailability always true).
Delivery
The buyer's Voucher carries plaintext Code (xxxx-xxx-xxx), plaintext Pin (6 digits), ClaimURL = the claim site, and 1-year expiry.
Catalog / push
Gift cards are Octopus fixed-amount topup variants projected into GH voucher_types by pushVariantToGrasshopper (admin_grasshopper.go:785): lookup existing type by (octopus_topup_product_id, denomination, name) → PUT/POST /api/voucher-types → upsert products+vendor_products (vendor=GH) → write the GH voucher_type id back onto vendor_products.vendor_product_id (that's the voucher_type_id sent at order time). Admin entry points: push / push-all (200ms throttle) / sync / upsert-from-topups (DB-only). HTTP helpers set a browser-like User-Agent to dodge Cloudflare Bot Fight Mode in front of the claim site.
Only fixed-amount variants are pushable (!IsFixedAmount || MinAmount==nil is skipped everywhere). Flexible-amount gift cards are admin-created manually. The GH↔Octopus link uses two separate id spaces (octopus_topup_product_id vs octopus_esim_product_id), only one populated per row.
Quirks
- Hashes not plaintext; inverted callback flow; GH holds no balance.
- Downstream is a Hono Worker on Cloudflare with a D1 (SQLite) DB (Drizzle migrations incl.
0005_inverted_voucher_flow.sql). CancelVoucher=DELETE /api/vouchers/:id;GenerateReferenceCode=GH_<uuid>.- Rate-limit aware (200ms throttles + bot-evading UA) because GH runs on Workers.
Tests
Go orchestration test test/clientapi/g2a_orchestration_test.go binds GH's host to an in-process httptest stub answering POST /api/vouchers with {"id":123456} (the GH "mock" analogue — deliberately not mocky-balboa). Worker test suite in frontend/grasshopper/test/ (claim verify/submit/notify/status, chaos/concurrent_claims). Deploy via deploy-grasshopper.yml.
Wupex — WPX (voucher)
Voucher vendor adapter — static API-key header, Cloudflare-fronted catalog with 429 backoff, USD-settled with currency inferred from product names.
Octopus (OCTO) — federated voucher
Voucher vendor adapter that consumes another Octopus instance's /api/v1 as a supplier — login-token auth, self-loop guard, free HMAC webhook.