Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Stellar Checkout is the open-source, non-custodial merchant checkout for the Stellar anchor network — the inbound counterpart to the Stellar Disbursement Platform.
Live on Stellar mainnet since 2026-09-06, payments-only. The first real
payment settled in ledger 64303816
(3e8dfd1…):
buyer wallet straight to merchant wallet, correlated by the link reference
carried in the transaction memo. Nothing was custodied in between, which is
checkable from that link without asking this service anything.README 전문 보기 ↗Stellar Checkout is the open-source, non-custodial merchant checkout for the Stellar anchor network — the inbound counterpart to the Stellar Disbursement Platform.
Live on Stellar mainnet since 2026-09-06, payments-only. The first real
payment settled in ledger 64303816
(3e8dfd1…):
buyer wallet straight to merchant wallet, correlated by the link reference
carried in the transaction memo. Nothing was custodied in between, which is
checkable from that link without asking this service anything.
Cash-out to local currency is not enabled on mainnet (OFFRAMP=none) —
sellers move their own funds. The off-ramp exists and runs a real
SEP-10 → SEP-38 → SEP-6 flow against testanchor.stellar.org on the testnet
deployment (dashboard ·
API), which is where to try it: create a
link, pay it from any testnet wallet with the shown memo, watch it flip to
paid. No real money moves there.
Checked by .github/workflows/uptime.yml — which
asks for every 5 minutes and, being a scheduled workflow on a free public repo,
actually gets somewhere between 30 minutes and 3 hours. It
publishes to the status
branch rather than committing to main every 5 minutes. The badges above and
the 90-day history read from there; the copy of docs/STATUS.md on main is a
snapshot and will lag.
The loop, end to end:
This is the non-custodial version of a hosted checkout (think Stripe-style PaymentIntent), built on the chain whose anchor network can actually settle to local rails.
Widget API reference: packages/widget/README.md.
Embed the lightweight modal checkout script tag in your HTML and attach it to any button:
<!-- Include widget script -->
<script src="https://quay-web.vercel.app/widget.js"></script>
<!-- Pay button bound to link ID -->
<button data-stellar-checkout="lnk_123">Pay with USDC</button>
<!-- Or trigger programmatically in JavaScript -->
<script>
StellarCheckout.open("lnk_123");
</script>
Self-hosting the widget? Point the script tag at your own deployment and
the widget infers the host from its own <script src> automatically - no
extra config needed. If you load widget.js some other way (bundled,
inlined, injected without a matching <script src="...widget.js"> tag),
Quay.open() cannot detect the host and will not guess — pass it explicitly:
Quay.open({ linkId: "lnk_123", host: "https://checkout.your-domain.com" });
A widget that can't determine its host throws a clear error rather than silently pointing at someone else's deployment.
Both write endpoints require authentication. Mint an API key from the dashboard (API keys → Create, after signing in with your Stellar wallet) and send it as a bearer token — the plaintext key is shown once, at creation.
Keys are scoped. The default set (links:read, links:write,
webhooks:manage) covers everything in this quickstart; offramp:initiate
moves money and must be requested explicitly.
Generate a payment link from your backend server:
curl -X POST https://quay-api.onrender.com/links \
-H "Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "T-shirt",
"amount": "10.50",
"assetCode": "USDC"
}'
Response (201 Created):
{
"link": {
"id": "lnk_123",
"reference": "ref_abc",
"status": "active",
"title": "T-shirt",
"amount": "10.50",
"asset": { "code": "USDC", "issuer": "GBBD456..." },
"destination": "GAHK789..."
},
"request": {
"uri": "web+stellar:pay?destination=GAHK789...&amount=10.50&memo=ref_abc",
"memo": "ref_abc",
"memoType": "text"
}
}
Register your endpoint to receive real-time JSON notifications when payments land on-chain:
curl -X POST https://quay-api.onrender.com/webhooks \
-H "Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-domain.com/api/webhooks/checkout" }'
Verify incoming HMAC-SHA256 signatures (x-checkout-signature: sha256=<hex>) in your webhook route (supports multiple comma-separated signatures during secret rotation and prevents length mismatch errors):
const crypto = require("crypto");
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return signatureHeader.split(",").some((part) => {
const a = Buffer.from(part.trim());
const b = Buffer.from(`sha256=${expected}`);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
Where this is going, and the argument for it, is in ROADMAP.md.
The link + checkout + on-chain payment is the easy, commodity part. The off-ramp is the hard 80% and the whole moat — and it isn't a step you bolt on, it's a corridor walking back in: FX rate risk in flight, KYC on the payout, reconciliation that proves local currency landed, recovery when the anchor is down.
So two deliberate boundaries are baked into the architecture:
Off-ramp runs seller_initiated, not inline. The seller receives the stablecoin to a
wallet they control and cashes out as a separate, authorized action. Custody stays at the
edges. inline mode (value routed through the anchor mid-flight, seller receives local
currency directly) is what merchants ultimately want — and it is the mode that puts you in
the money-transmission / custody box. The OffRampPort already models both modes; do not
flip to inline until a licensed anchor relationship and a compliance story are real.
Ports-and-adapters everywhere. The domain never imports a chain SDK. RailPort,
WatcherPort, and OffRampPort are the seams. Today: a Stellar (SEP-7 + Horizon) rail and
four off-ramp modes — a real anchor flow (SEP-10/12/38 plus SEP-6 or SEP-24, chosen per anchor), a mock
for offline dev, and a disabled mode for mainnet. Tomorrow: the same PaymentIntent spine
behind an adapter-gateway (Arc/Circle) or a different chain — without touching the domain or
the worker.
"Non-custodial" should be a claim you can check, not one you have to trust. This is everything the Quay server holds, where it lives in the code, and what it can do — verify each row against the cited file:
| Held by the server | Where | Can it move a seller's funds? |
|---|---|---|
SERVER_SIGNING_SECRET — signs SEP-10 login challenges for the dashboard | apps/api/src/env.ts (serverSigningSecret), used in apps/api/src/services/container.ts | No. It is an identity key for authentication; it is not a signer on any seller account |
JWT_SECRET — signs Quay session JWTs | apps/api/src/env.ts (jwtSecret) | No |
| Each seller's anchor SEP-10 JWT, AES-256-GCM encrypted at rest | anchor_sessions.token_encrypted in apps/api/src/db/schema.ts; encryption in apps/api/src/repos/index.ts | No. See the paragraph below for exactly what it can do |
Seller SEP-12 KYC fields, encrypted with KYC_ENCRYPTION_KEY | seller_kyc.fields_encrypted in apps/api/src/db/schema.ts | No |
Seller payout fields (bank details), AES-256-GCM encrypted at rest with KYC_ENCRYPTION_KEY | sellers.payout_fields_encrypted in apps/api/src/db/schema.ts (legacy payout_fields_json is deprecated) |
The rule all of this adds up to: no key on the server can sign a payment
from a seller's account or a withdrawal transfer — sellers sign with their own
wallet, in the browser
(apps/web/lib/wallet.ts, sendAnchorTransfer).
The one credential that deserves elaboration is the anchor SEP-10 JWT. It
can read and update the seller's KYC record at the anchor and start a
withdrawal (SEP-6), which yields payment instructions. It cannot move money:
the on-chain leg of a cash-out is a Stellar transaction that only the seller's
wallet can sign. The token is also never returned to the browser — the API
relays the anchor challenge for the seller's wallet to sign, then keeps the
resulting JWT server-side, encrypted
(apps/api/src/routes/anchor-auth.ts).
You can check the core claim without trusting this README or this service:
the mainnet transaction linked at the top of this file shows the buyer's
wallet paying the seller's wallet directly, with no intermediate account. That
is the whole payment path — checkout pages expose the seller's own
destination address (apps/api/src/routes/links.ts,
toCheckoutView) and buyers pay it straight from their wallet.
Threat handling and reporting live in SECURITY.md; the full
component and flow detail is in docs/ARCHITECTURE.md.
packages/
core/ Domain brain — entities, status machine, money math, SEP-7 builder,
the pure payment matcher, port interfaces, zod schemas.
stellar/ Stellar adapter — SEP-7 rail + Horizon polling watcher (RailPort/WatcherPort).
offramp/ Off-ramp adapters (OffRampPort, seller_initiated) — TestAnchorOffRamp
(real SEP-10 → SEP-38 → SEP-6, used for testanchor.stellar.org and production
anchors alike), AnchorOffRamp (SEP-24 interactive), SelectingOffRamp (picks SEP-6
or SEP-24 per anchor), MockAnchorOffRamp (offline dev), DisabledOffRamp (mainnet),
plus TestAnchorKyc (SEP-12) and SellerAnchorAuth (per-seller SEP-10 anchor sessions).
widget/ Source of the embeddable checkout widget (built and served as widget.js).
apps/
api/ Hono API + Drizzle (libSQL) + the ledger-watching worker.
web/ Next.js (App Router) seller dashboard + buyer checkout page + widget.js.
core is the only package with business logic worth unit-testing in isolation, and it is:
money is compared in integer stroops (never floats), the status machine rejects illegal
transitions, the SEP-7 builder is spec-checked, and the matcher is exhaustively tested for
paid / overpaid / underpaid / wrong-asset / no-memo / unknown-reference.
Requirements: Node 22 (what CI and the API's Docker image run) and pnpm 9.
pnpm install
cp .env.example .env
Two processes (two terminals):
# 1) API + ledger watcher → http://localhost:8787
pnpm --filter @checkout/api dev
# 2) Web dashboard + checkout → http://localhost:3000
pnpm --filter @checkout/web dev
On first boot on testnet with no DEFAULT_SELLER_WALLET set, the API generates a
throwaway keypair, prints it, and gives you a Friendbot link to fund it — so pnpm dev
needs no configuration. Set DEFAULT_SELLER_WALLET in .env to reuse a stable address
across restarts.
The variable is optional everywhere and names nobody's funds: Quay is multi-tenant, so a
seller signs in with their own wallet over SEP-10 and every link they create is paid straight
to that address. Setting it only gives /health one wallet whose USDC trustline it can
report.
Then: open the dashboard, create a link, open its checkout page, and pay the displayed amount
of USDC with the shown memo from any Stellar testnet wallet. Within a poll interval the
dashboard flips the link to paid; hit the Cash out button to exercise the off-ramp seam
(labelled with NEXT_PUBLIC_OFFRAMP_CURRENCY — NGN with the default OFFRAMP=mock; set it to
USD when OFFRAMP=testanchor, since the testnet anchor only quotes USDC against USD/CAD).
Useful scripts (from the repo root):
pnpm typecheck # all packages
pnpm test # every package's test suite, via turbo
pnpm test:coverage # the same tests, with the CI coverage gate applied
pnpm build # builds the web app
pnpm sweep # pre-entry ritual: uptime + synthetic checks against the live demo
Coverage gating. pnpm test:coverage fails if any package drops below the
floor in its vitest.config.ts, and CI runs it on every push. Those floors are
a ratchet: each was set to that package's measured coverage when gating (or a
later raise) landed — the per-package vitest.config.ts is the source of
truth for the current numbers, so none are repeated here to drift. Raise a
floor when you raise the coverage; never lower one to make a build pass — that
is the single move the gate exists to prevent. The run also uploads an HTML
report as a CI artifact and prints a per-package table to the job summary.
Instead of starting from a blank screen, seed the dashboard with real on-chain data in about a
minute. It reads STELLAR_NETWORK the same way the API does (defaulting to testnet), so it
works against a mainnet or self-hosted deployment too — not just testnet:
# With the API already running on http://localhost:8787:
pnpm demo:seed
Prerequisite: DEFAULT_SELLER_SECRET in .env (matching the DEFAULT_SELLER_WALLET the API
is running with). Since auth landed, every /links route is seller-authenticated, so the script
logs in as the configured demo seller via SEP-10 and creates the links under that same seller —
otherwise the dashboard you already know would have nothing to show.
What it does:
POST /links (flagged as demo data). The /demo
storefront page's "Pay with Quay" button reads the first of these from GET /demo/link,
so it points at a link that actually exists rather than a hardcoded id.offramp_settled row.Every seeded row is real on-chain data — nothing is written directly to the database. Demo rows are labelled with a demo badge in the dashboard so they are easy to tell apart from links you create yourself.
# Remove all demo-flagged rows:
pnpm demo:reset
demo:reset calls POST /demo/reset on the running API. It's testnet-only (returns 403 on
the public network) and requires a seller session, so the script logs in as the same demo
seller using DEFAULT_SELLER_SECRET.
apps/api)apps/api/Dockerfile is a 3-stage build (deps → build → runtime): the
runtime stage carries only apps/api's real npm production dependencies (all
workspace source is bundled into one compiled dist/index.js via esbuild -
see the build script in apps/api/package.json) and runs as the non-root
node user.
node:22-alpine@sha256:..., see the
ARG BASE_IMAGE at the top of the Dockerfile) - bump it at least monthly,
or immediately on a disclosed CVE, per that same comment.tini runs as PID 1 (ENTRYPOINT) so SIGTERM actually
reaches the Node process, which drains in-flight HTTP requests before
exiting (apps/api/src/index.ts)./health is liveness (process is up); /ready is readiness
(database is actually reachable) - the HEALTHCHECK and Render's
healthCheckPath both use /ready..github/workflows/ci.yml's
docker job fails the build if it isn't, checking against BUDGET_MB=250).docker run --read-only --tmpfs /tmp or an orchestrator's
equivalent read-only-root setting - Render's blueprint format has no field
for this today, so it isn't set in render.yaml, but nothing in the image
requires a writable root filesystem.| Piece | Status |
|---|---|
| SEP-7 payment-request URIs | Real, spec-correct (native vs issued asset, memo ≤28 bytes, %20 encoding, network passphrase). |
| Horizon payment watching + memo matching | Real logic against the Stellar SDK v16 API. Polling (restart-safe), idempotent via persisted cursor + processed-tx ledger. One Horizon request per page (join=transactions for the memo lookup, not one-plus-N), and a transaction-fetch failure retries the tick rather than silently parking a matchable payment as no_memo. Every Horizon call retries transient failures (3 attempts, exponential backoff + full jitter, honors Retry-After on 429) and can fail over to HORIZON_URL_FALLBACK; sustained failure shows up in GET /health instead of silently going idle. |
| Status lifecycle, webhooks (HMAC-SHA256 signed) | Real. |
| Persistence | Real, libSQL/SQLite — zero-config locally (file:./local.db), and the same DATABASE_URL points at Turso in production. Note this is libSQL specifically, not "any SQL": the schema is drizzle-orm/sqlite-core and the client is @libsql/client, so a postgres:// URL does not work without porting the schema, the bootstrap DDL and the backup scripts. Tables self-initialize on boot. Encrypted backups (pnpm db:backup) and a tested restore path (pnpm db:restore) exist — see the runbook for the honest RPO/RTO (nightly backups ⇒ up to 24h RPO, not continuous protection). |
| Account/trustline preflight | Real. POST /links checks the seller's wallet actually exists and (for USDC) has an authorized, under-limit trustline before the link goes live — 422 destination_cannot_receive otherwise, with a SEP-7 deep link to add the trustline. Re-checked in GET /health so a revoked trustline shows up in ops, not as a dead checkout page. |
Off-ramp (@checkout/offramp) | Real, opt-in. Set OFFRAMP=testanchor for a genuine SEP-10 → SEP-12 → SEP-38 → SEP-6 flow against the public Stellar testnet anchor (https://testanchor.stellar.org), or OFFRAMP=anchor for a production anchor; the withdrawal runs over SEP-6 or SEP-24 depending on what the anchor declares (, default ), and more anchors are added per corridor with . Defaults to (, fake FX rate, no money moves) for offline dev — the dashboard labels the cash-out button "(simulated)" whenever mock mode is active. |
.env.example ships Circle issuers for testnet and
public. Confirm the current issuer for your network before relying on it — a wrong issuer
silently matches nothing (or the wrong asset).packages/offramp/src/testanchor.ts is a real SEP-10 → SEP-38 → SEP-6 adapter, but
against Stellar's public testnet reference sandbox — not a licensed anchor. Fork its shape
for a production adapter against a licensed Nigerian anchor's SEP endpoints, and validate the
anchor will actually onboard you and pay out before building further.inline off-ramp without legal review. See the boundary note above.WatcherPort implementation (WATCH_MODE=stream, opt-in until you have measured it).This README is engineering guidance, not legal advice. Money transmission is the box you do not want to back into by accident.
Licensed under the Apache License 2.0.
| Webhook signing secrets (current and previous), encrypted | webhooks.secret_encrypted / previous_secret_encrypted in apps/api/src/db/schema.ts | No |
OFFRAMP_PROTOCOLautonode scripts/anchors.mjsOFFRAMP=mockMockAnchorOffRamp| Metrics | Real. GET /metrics (Prometheus text format, METRICS_TOKEN-gated) — payment/webhook/anchor counters, watcher-lag and latency histograms, a circuit breaker around the off-ramp adapter. See docs/API.md and docs/grafana-dashboard.json. |
Embeddable widget (/widget.js) | Real, lightweight embeddable script rendering modal checkout. |
| Auth | Real, and enforced. SEP-10 wallet login (GET/POST /auth) issues a short-lived session JWT (sub, sellerId, jti, exp ≤ 24h) and sets it as an httpOnly cookie; seller-authenticated middleware gates every /webhooks route and every /links route except the public checkout reads and submit (GET /links/:id, POST /links/:id/submit) and GET /r/:reference (401 unauthenticated, 404 for another seller's link, so ids are not confirmed), POST /auth/logout revokes a token by jti. Wallet-connect is wired in the dashboard and is how a seller signs in: the address they authenticate with becomes both their identity and their payout destination, so POST /links sets destination from the authenticated seller rather than from configuration. See docs/API.md. |