The agentic card

Documentation

GlassPay issues scoped, revocable spending cards from your wallet. Any agent plugs one in over MCP and pays within your limits, holding no keys and no funds, dead the moment you revoke. Here is how it works, end to end.

Overview

Agents need to spend money. Handing an agent your private key is reckless; funding a standalone agent wallet loses both your custody and your limits. GlassPay takes the model the card industry settled on decades ago and applies it to agents: the wallet stays the account, and the agent gets a card, a scoped authority to draw from it.

  • Your wallet is the account. Funds never leave it until the moment of payment.
  • The card is a delegation. A scoped ERC-7710 delegation, signed by your wallet, wrapped in caveats: budget per period, per-transaction cap, merchant allowlist, expiry, usage count, contract scope.
  • The agent holds the card, not the money. What the agent gets is an MCP endpoint URL. Behind it, the card can spend only what its terms allow, signed by an agent key that holds nothing.
  • Revoke kills it instantly. Freeze or revoke a card (or its whole sub-card tree) and every payment stops, server-side immediately and on-chain underneath.
your wallet (EIP-7702 smart account) └── card $25 / week · expires Jul 6 ← root delegation, signed by you ├── agent A plugs it in over MCP └── sub-card $1 / week · one merchant ← redelegation, narrower terms └── sub-agent B plugs it in

GlassPay runs on Base mainnet with real USDC. The only simulated leg is the Visa rail (Stripe test-mode Issuing), labeled honestly wherever it appears.

Dashboard
glasspay (your deployment)
API + MCP
glasspay-api (your deployment)
Demo merchant

How a Payment Works

  • You sign in to the dashboard (Privy embedded wallet, Google or email) and issue a card with terms, set by hand in the composer or drafted from plain language by the Venice-powered compiler.
  • The dashboard compiles those terms into on-chain caveats, your wallet signs the delegation in the browser, and the server stores it alongside a fresh agent key that holds nothing.
  • You hand the card URL to any agent (one claude mcp add, a Cursor deeplink, a pasted connector URL).
  • When the agent calls pay, the server validates the terms, then redeems the delegation through the 1Shot Public Relayer: gasless, on Base mainnet, settled in USDC from your wallet.
  • Every charge lands in the card's ledger with memo, fee, and tx hash, attributed to the agent key that spent it.

The agent never sees a private key, never holds a balance, never needs ETH. The first spend even deploys your wallet's EIP-7702 smart-account code automatically, attached to the same redemption as an authorization list.

Issuing a Card

A card is born from a CardTerms object: a pay budget, a contract scope, or both, plus lifecycle limits (expiry, max uses, per-charge cap, merchant lock, sub-cards on/off). You can write the terms by hand in the composer, or describe the card in plain language and let the compiler draft them.

The plain-language compiler

The dashboard's issue modal sends your sentence to Venice AI, which returns a plan of named entities ("USDC", "Uniswap", "aave") and numbers. The server then resolves every name against its own verified registry (or Basescan, or your own pasted address), so the model output can never place a raw address into a draft. The result is a CardTerms draft you review and sign; nothing is issued until you do.

The compiler only names tokens, protocols and merchants. Addresses come exclusively from the trusted resolvers or your own text, with provenance shown on each chip (registry, Basescan, or your input). A draft cannot smuggle a poisoned address even if the model tries.

The client-signed ceremony

Issuance is a three-step prepare / sign / finalize so the server never holds your key:

  • prepare: the server compiles the caveats, mints the agent key, and returns the exact unsigned delegation struct.
  • sign: your embedded wallet signs the EIP-712 delegation in the browser.
  • finalize: the server verifies the signature recovers to your wallet, then persists the card and returns its URL.

Card Terms

Each term compiles to a delegation-framework enforcer caveat at the root of the delegation, so the chain enforces the same limits the server checks. Below is the exact mapping (engine/src/compiler.ts).

TermMeaningOn-chain enforcer
pay.periodBudget per rolling window (amount + seconds, min 60s)ERC20PeriodTransfer
pay.lifetimeTotal USDC the card may ever moveERC20TransferAmount
contract.targetsContracts the card may callAllowedTargets
contract.selectorsMethod signatures the card may callAllowedMethods
expiryUnix time after which nothing redeemsTimestamp
maxUsesRedemption count (scaled to executions on-chain; server is the binding limit)LimitedCalls
revocation nonceAlways present; bumping it nukes every card from this walletNonce
pay + contractA composite card; one group governs each redemptionLogicalOrWrapper

perTxMax and merchantsare not root caveats; they collide with the mandatory fee leg there. They are server-side carve policy applied at redemption: the per-transaction max is backstopped on-chain by the carved leaf's amount scope, while the merchant allowlist is enforced server-side. contract.tokens and contract.perTradeMax additionally pin each ERC-20 allowance to an exact spender and amount via byte-window AllowedCalldata caveats on that leaf.

Connecting an Agent

The card is served over MCP (Streamable HTTP). There are three connection lanes. The first two carry a per-card credential directly; the third is OAuth, where the agent never holds the card secret.

ASecret in the URL path

Works everywhere, including credential-free clients like claude.ai web. The URL is the password, treat it like one.

claude mcp add --transport http glasspay \
  https://<host>/c/<card-secret>/mcp
BBearer header

For clients that send an Authorization header. The bare endpoint, secret in the header.

claude mcp add --transport http glasspay \
  https://<host>/mcp \
  --header "Authorization: Bearer <card-secret>"
COAuth 2.1 (card-picker consent)

Add the bare endpoint with no credential. The client discovers the OAuth lane (RFC 9728 metadata on the 401), registers itself (dynamic client registration), and opens a browser; you sign in and pick which card to grant. The agent receives a short-lived, card-scoped, independently revocable token, never the raw secret. This is the lane OAuth-only clients such as ChatGPT require. Clients that finish OAuth out of band read the code off the consent screen: OpenClaw completes with openclaw mcp login glasspay --code <code>, and headless Hermes uses the same paste-back.

claude mcp add --transport http glasspay https://<host>/mcp

Per-harness one-liners (Lane A)

codex     mcp add glasspay --url https://<host>/c/<secret>/mcp
openclaw  mcp add glasspay --url https://<host>/c/<secret>/mcp --transport streamable-http  # flag required: omitting it defaults to SSE
hermes    mcp add glasspay --url "https://<host>/c/<secret>/mcp"
gemini    mcp add -t http glasspay https://<host>/c/<secret>/mcp
goose     session --with-streamable-http-extension "https://<host>/c/<secret>/mcp"
amp       mcp add glasspay https://<host>/c/<secret>/mcp
droid     mcp add glasspay https://<host>/c/<secret>/mcp --type http

Lanes A and B work in Cursor, VS Code, Gemini CLI, Windsurf, claude.ai custom connectors, or any MCP client that speaks Streamable HTTP. For claude.ai web, paste the card URL under Customize → Connectors → Add custom connector; for ChatGPT Developer Mode, add it as a No Authentication connector (or use Lane C for a real auth story). The dashboard's connect panel renders a prefilled install affordance per harness. Rotate the secret any time from the dashboard; the old URL dies instantly.

MCP Tools

The tool list a card exposes is its permission surface: a pay-only card never sees execute; a contract-only card never sees pay; a sub-cards-off card never sees issue_subcard. The server is stateless, a fresh instance per request, identity = the card credential.

ToolOnPurpose
cardEvery cardLive state: remaining budget, terms, expiry, recent charges, sub-cards, and the card's on-chain account (the root delegator that holds the USDC and receives contract-call output). Call it first.
paypay cardsSend USDC on Base within limits; blocks until confirmed on-chain.
paid_fetchpay cardsFetch a URL; on HTTP 402 (x402), pay automatically and return the content.
fiat_paypay + StripeBuy over Visa rails (simulated) against the same budget; with settlement on, the receipt carries the on-chain tx.
card_credentialspay + StripeReveal the test-mode virtual Visa for a merchant checkout; every card auto-links one on first need.
executecontract cardsRun scoped contract calls (approve + swap, stake, mint) atomically in one redemption.
issue_subcardsub-cards onMint a tighter child card for a sub-agent; omitted money terms inherit the parent's remaining budget; returns its URL.
revoke_subcardsub-cards onInstantly kill a sub-card and its descendants (server-side).

Typed refusals

Refusals come back as isError with structured JSON naming the violated term, so an agent can relay them honestly instead of guessing. The codes include:

  • over_period_limit, merchant_not_allowed, price_exceeds_max (pay and paid_fetch)
  • target_not_allowed, method_not_allowed, per_trade_exceeded, token_not_allowed, spender_not_allowed (execute)
  • exceeds_parent_terms (issue_subcard); not_your_subcard (revoke_subcard)
  • card_frozen, no_fiat_card (the fiat leg); invalid_terms (bad input)

Contract Cards

A card can be scoped to specific contract targets and method selectors instead of (or alongside) a USDC budget. The agent calls execute with either {target, method, args} (the server ABI-encodes the calldata) or {target, data} raw calldata for tuple/array/ multicall methods like Uniswap exactInputSingle.

  • Targets and selectors outside the card's declared scope are refused before anything reaches the chain; the on-chain AllowedTargets / AllowedMethods enforcers check the same scope again at redemption.
  • Method signatures are normalized to canonical form (uintuint256) so the encoder, the raw-data selector check, and the on-chain enforcer all agree.
  • A contract card can carry an allowance token list (contract.tokens: the only tokens it may approve, each approval pinned on-chain to an exact spender and amount; the listed tokens are auto-unioned into the card's targets) and a per-trade ceiling (contract.perTradeMax, capping each USDC approval; v1 enforces the ceiling on the USDC / settlement leg only, while non-USDC approvals stay exact-amount pinned).
  • For a call that needs a recipient (e.g. exactInputSingle's recipient), the card tool surfaces the card's on-chain account(the root delegator that holds the USDC and receives any output tokens), so the agent routes a swap's output there itself rather than guessing or asking the user.
  • Contract calls carry no native ETH value in v1 (the carved leaf caps value at 0 on-chain); up to 5 calls batch atomically into one redemption.

Contract calls are not USDC-metered. Safety on a contract card is the target/method allowlist plus maxUses and expiry. Pair contract scope with a pay cap in one composite card when you want both rails under one delegation.

Sub-Cards & Revocation

Sub-cards are ERC-7710 redelegations. An agent holding a card can mint a tighter child for a sub-agent with issue_subcard; every term must fit inside the parent's (caps only narrow downward, contract scope is subset-only, never silently inherited). exceeds_parent_terms names the violating field. The chain enforces the same subset via the delegation chain.

Three layers of off-switch

LayerEffectWhere
FreezeReversible pause; the card still answers card but refuses spendsServer-side, instant
RevokePermanent; the card and its whole sub-card subtree dieOn-chain disableDelegation, signed by your wallet
NukeKills every card and sub-card this wallet ever issuedOne on-chain NonceEnforcer bump

All three are user-operable from the dashboard. On-chain revoke and nuke are signed by your own embedded wallet in the browser (an admin leaf delegation) and ride the relayer gaslessly. Revoking a parent kills the subtree; the cascade is the demo money-shot, revoke the root and the whole tree dies on screen.

An agent's own revoke_subcardis a server-side kill: instant, and the sub-card's URL dies, but a sub-card cannot be disabled on-chain on its own (its on-chain delegator is the parent's agent key). On-chain permanence for a whole branch comes from revoking the root card or nuking.

Payment Rails

Two payment rails run off one delegation, metered by the same enforcers.

x402 (real, live)

paid_fetchanswers an HTTP 402 challenge by paying through the card's 7710 delegation: real x402 v2 flows on Base mainnet, USDC settled from your wallet through the 1Shot Public Relayer (gasless, fee in USDC). GlassPay also ships the first ERC-7710 x402 facilitator (/facilitator/verify, /settle, /supported advertising assetTransferMethod: erc7710) and a demo seller at /demo/premium-data whose 402 points back at it.

Stripe Issuing Visa (simulated)

fiat_pay and card_credentialsdrive a test-mode virtual Visa. When a charge is authorized, Stripe calls GlassPay's real-time auth webhook, which answers approve/decline from the card's on-chain delegation state inside Stripe's hard 2-second window (read from a cached snapshot, never an RPC call in the handler). A decline comes back typed, from the card's terms, not the merchant.

With settlement enabled, an approved Visa charge then settles as a real delegated USDC transfer on Base, through the same enforcers that meter the crypto rail. One budget, two rails. A charge whose settlement cannot land parks settlement_unconfirmed and freezes the card rather than ever releasing its budget.

The Visa leg is simulated by design: Stripe test-mode Issuing, no real merchant, no KYC required in test. It is labeled honestly everywhere it appears. The crypto rail and the on-chain settlement move real USDC on Base mainnet.

The demo merchant, s0nder supply co. at /shop, is a real storefront that accepts the cards' Visas. The catalog is priced at $5 or less because approved purchases move real USDC.

Security

  • Custody.Your funds stay in your wallet. The per-card agent key signs redelegations only; it holds no assets and is encrypted at rest. You can export your wallet's private key from the account menu at any time (through Privy's secure modal, rendered in a separate-domainiframe GlassPay never reads) and walk away to any client.
  • Dashboard auth. Per-user Privy sessions, verified server-side against the app JWKS. At onboard, the embedded wallet signs glasspay-onboard:v1:<did>to bind the wallet to that login; every card route is then scoped to the authenticated user's own cards.
  • Issuance integrity. The server verifies the delegation signature recovers to the delegator before persisting a card.
  • Card secrets. 256-bit, stored as a hash for auth and AES-256-GCM-encrypted at rest for the reveal/rotate feature. The URL is a credential; rotate it like a password.
  • Limits enforced twice.Server-side at call time (typed refusals) and on-chain at redemption. Period, lifetime, expiry, usage count and contract target/method have dedicated on-chain enforcers; the per-transaction max and merchant allowlist are server-side policy, backstopped on-chain by the carved leaf's amount scope.
  • MCP surface hardening. Host allowlist (DNS-rebinding guard), per-card and bad-secret rate limits, a 1 MiB body cap, an SSRF guard on paid_fetch targets, secrets never echoed in errors or logs.
  • OAuth tokens. Opaque, card-scoped, hash-stored beside the card secrets, audience-pinned (RFC 8707), and revoked the instant the card is, cascading to the subtree.

SigNoz Observability

GlassPay is fully instrumented with OpenTelemetry and ships traces, metrics, and logs to SigNoz Cloud (and can self-host locally via the included casting.yaml). Every card issuance, every payment, every refusal, every API call — all visible in SigNoz.

Architecture

glasspay-server (Bun ─preload otel.ts) │ ├─ @opentelemetry/auto-instrumentations-node │ · HTTP spans (every API call) │ · fetch spans (outbound requests) │ · DNS, filesystem, database │ ├─ Manual business spans (trace API) │ · stripe_webhook_auth (approve/decline decision) │ · 1shot_relayer_redeem (on-chain payment) │ · nl_compile (AI card compilation) │ · reconcile_sweep (stuck charge resolution) │ · fiat_settle_sweep (Visa→USDC settlement) │ ├─ Custom metrics (Meter API) │ · counters: cards_issued, charges, errors, usdc_spent │ · gauge: active_cards │ ├─ Structured logs (Logger API) │ · card_event: issued, frozen, revoked, nuked, onboarded │ · charge_event: confirmed, refused, pending │ · operation: every API error with route, method, status │ └─ OTLP HTTP exporter → SigNoz Cloud (or local :4318)

Env vars (already set on Railway)

OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.us2.signoz.cloud
OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=YOUR_KEY
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp

The OTel SDK initializes early via Bun --preload (packages/server/src/otel.ts) so auto-instrumentation wraps every module from boot. The engine package (packages/engine/src/telemetry.ts) declares all custom metrics and structured log functions — 5 counters, 2 log emitters, available for any SigNoz dashboard panel.

Traces — Distributed Tracing

Every API request is wrapped in a root span by Hono middleware (app.ts) with attributes for route pattern, HTTP method, URL, and response status code. Inside those (and running on their own intervals), six custom business-logic spans carry domain-specific attributes.

Route-level spans (every request)

The Hono app.use("*", ...) middleware creates a span for every request, named HTTP {METHOD} {ROUTE}. Navigate to SigNoz → Traces, filter by service.name = glasspay-server, and see every API call with its duration, status, and route pattern.

stripe_webhook_auth

Fires when Stripe calls the real-time auth webhook. Attributes: decision (approve/decline), card_id, amount, merchant. Traces the full auth decision flow inside Stripe's 2-second window.

1shot_relayer_redeem

Fires on every on-chain payment. Attributes: usdc_amount (string), gas_fee_usdc, memo, tx_hash, card_id. Shows the full lifecycle of a USDC redemption through the 1Shot Public Relayer.

nl_compile

Fires when Venice AI compiles a plain-language card request. Attributes: prompt_tokens, completion_tokens, model. Tracks AI usage for cost monitoring.

reconcile_sweep

Runs on a configurable interval (default 5 min). Attributes: reconciled (count), still_pending (count). Resolves stuck pending charges against chain truth.

fiat_settle_sweep

Runs on a configurable interval (default 60s). Attributes: settled (count), left (count). Settles approved Visa charges as on-chain USDC transfers.

Metrics — Product KPIs

Five custom counters are available in SigNoz Metrics. Navigate to SigNoz → Metrics and search for any of the following metric names to build dashboard panels.

Metric NameTypeDescription
glasspay_cards_issued_totalCounterTotal cards issued across all users (root + sub-cards). Increments on issue, finalize, and sub-card mint.
glasspay_usdc_spent_totalCounterTotal USDC spent across all confirmed redemptions and fiat settlements. The dollar volume metric.
glasspay_active_cardsUpDownCounterCurrent live cards (issued − revoked). A gauge: add 1 on issue, subtract 1 on revoke/nuke.
glasspay_charges_totalCounterTotal charges processed (confirmed + pending + failed). Payment throughput metric.
glasspay_errors_totalCounterTotal API-level errors (403 refusals, 422 validation errors, 502 relay failures, 500 exceptions).

These metrics are created in packages/engine/src/telemetry.ts using the OpenTelemetry Metrics API and exported via OTLP HTTP to SigNoz. They appear in the Metrics explorer under their metric names, prefixed by the engine package.

Building a metric panel

In SigNoz, create a new dashboard, click New Panel, choose Time Series, then switch to the ClickHouse query tab. The SigNoz metrics storage uses the signoz_metrics.distributed_samples_v2 table. Example query for cards issued:

SELECT toStartOfInterval(
         toDateTime(intDiv(timestamp_ms, 1000)),
         INTERVAL 5 MINUTE) AS ts,
       sum(value) AS value
FROM signoz_metrics.distributed_samples_v2
WHERE metric_name = 'glasspay_cards_issued_total'
  AND ts BETWEEN $start_datetime AND $end_datetime
GROUP BY ts
ORDER BY ts

Structured Logs — Event-Driven Observability

GlassPay emits structured logs for every significant card lifecycle event. Navigate to SigNoz → Logs and filter by card_event, charge_event, refusal_reason, or operation to see exactly what happened.

Card lifecycle events

AttributeEventsContext
card_eventissued, frozen, unfrozen, revoked, nuked, url_revealed, secret_rotated, onboardedEvery card lifecycle transition with card_id and extra attributes (k_agent_address, address, has_auth7702 for onboarded).
charge_eventconfirmedSuccessful payments with amount (string), kind (crypto/fiat), and card_id.
refusal_reasonover_period_limit, merchant_not_allowed, price_exceeds_max, card_frozen, …Typed refusal with attempted_amount and card_id. Every declined payment leaves a structured trace.
operationHTTP {METHOD} {ROUTE}API error logs with status code, error_message, and route info. Every 403/422/502/500 is logged.

Example log searches

  • card_event: "issued" — see every card being created in real-time
  • card_event: "revoked" — track card revocations
  • refusal_reason: * — all payment refusals with typed reasons
  • charge_event: "confirmed" — successful payments with amounts
  • operation: * — all API errors grouped by operation

Logs are emitted using the OpenTelemetry Logger API (@opentelemetry/api-logs) through the logger.emit() function. Each log carries its severityText (INFO, WARN, ERROR) and structured attributes that SigNoz indexes automatically.

SigNoz Dashboard & ClickHouse Queries

Create a GlassPay dashboard in SigNoz with panels for every metric and trace attribute. Below are the ClickHouse queries for each panel type.

Panel 1: Cards Issued Over Time

Panel Type: Time Series. Shows the rate of card issuances over time.

SELECT toStartOfInterval(
         toDateTime(intDiv(timestamp_ms, 1000)),
         INTERVAL 5 MINUTE) AS ts,
       sum(value) AS value
FROM signoz_metrics.distributed_samples_v2
WHERE metric_name = 'glasspay_cards_issued_total'
  AND ts BETWEEN $start_datetime AND $end_datetime
GROUP BY ts
ORDER BY ts

Panel 2: Active Cards (Gauge)

Panel Type: Value (big number). Shows the current live card count.

SELECT sum(value) AS active_cards
FROM signoz_metrics.distributed_samples_v2
WHERE metric_name = 'glasspay_active_cards'
  AND timestamp_ms > toUnixTimestamp(now()) * 1000 - 60000

Panel 3: USDC Spent

Panel Type: Time Series. Tracks cumulative USDC volume.

SELECT toStartOfInterval(
         toDateTime(intDiv(timestamp_ms, 1000)),
         INTERVAL 5 MINUTE) AS ts,
       sum(value) AS value
FROM signoz_metrics.distributed_samples_v2
WHERE metric_name = 'glasspay_usdc_spent_total'
  AND ts BETWEEN $start_datetime AND $end_datetime
GROUP BY ts
ORDER BY ts

Panel 4: API Error Rate

Panel Type: Time Series. Track error spikes.

SELECT toStartOfInterval(
         toDateTime(intDiv(timestamp_ms, 1000)),
         INTERVAL 5 MINUTE) AS ts,
       sum(value) AS errors
FROM signoz_metrics.distributed_samples_v2
WHERE metric_name = 'glasspay_errors_total'
  AND ts BETWEEN $start_datetime AND $end_datetime
GROUP BY ts
ORDER BY ts

Panel 5: API Request Duration by Route

Panel Type: Time Series. Uses trace data to show P99 latency per API route.

SELECT toStartOfInterval(timestamp, INTERVAL 5 MINUTE) AS ts,
       attributes_string['http.route'] AS route,
       avg(durationNano) / 1000000 AS avg_ms
FROM signoz_traces.distributed_signoz_index_v2
WHERE resources_string['service.name'] = 'glasspay-server'
  AND ts BETWEEN $start_datetime AND $end_datetime
GROUP BY ts, route
ORDER BY ts

The bodyAttributes access pattern varies by SigNoz version. In newer versions, use bodyAttributes['http.route']. The $start_datetime and $end_datetime variables are automatically provided by the SigNoz dashboard panel builder.

SigNoz MCP — AI-Agent Observability

The included casting.yaml ships a SigNoz MCP server alongside the SigNoz stack. This lets your AI agent query traces, logs, and metrics directly — and even create dashboards and alerts autonomously.

Connect your agent to SigNoz MCP

# Self-hosted (from casting.yaml):
claude mcp add signoz http://localhost:8000 \
  --header "Authorization: Bearer glasspay_mcp_dev"

# SigNoz Cloud (API-based):
claude mcp add signoz https://signoz.io/api/mcp \
  --header "Authorization: Bearer $YOUR_SIGNOZ_API_KEY"

Available MCP tools

Once connected, your agent can use SigNoz MCP tools for observability workflows:

  • signoz_search_docs — Search SigNoz documentation for guides and references.
  • signoz_create_dashboard — Create new dashboards with panels for GlassPay metrics.
  • signoz_modify_dashboard — Update existing dashboard panels and configurations.
  • signoz_create_alert — Set up alerts for error rate spikes, card issuance stalls, etc.
  • signoz_investigate_alert — Deep-dive into alert-triggered incidents with neighbor signals.
  • signoz_generate_query — Generate ClickHouse queries for GlassPay observability data.
  • signoz_explain_dashboard — Understand existing dashboard layouts and panel semantics.
  • signoz_manage_views — Create and manage saved views for quick data exploration.

Example: ask your agent "Create a SigNoz dashboard for GlassPay showing cards issued, USDC spent, and API error rate" — it will use the MCP tools to build the entire dashboard without you touching the SigNoz UI.

Alerts & Self-Hosting

Recommended alerts

Set up these alerts in SigNoz to monitor GlassPay health:

AlertConditionSeverity
High Error Rateglasspay_errors_total rate > 10/min for 5 minCritical
No Cards Issuedglasspay_cards_issued_total has no new value for 30 minWarning
High API LatencyP99 HTTP duration > 5000ms for 5 minWarning
Refusal SpikeLog count with refusal_reason:* > 20/minWarning
Charge Failurecharge_event:confirmed rate drops by 50% vs previous hourCritical

Self-hosted SigNoz (local development)

The repo includes a casting.yaml for deploying SigNoz locally using Foundry. This is perfect for development and testing without sending telemetry to the cloud.

# Deploy the full SigNoz stack:
foundryctl cast -f casting.yaml --locked

# SigNoz UI:    http://localhost:3301
# OTLP HTTP:    http://localhost:4318
# SigNoz MCP:   http://localhost:8000

# Then set the server env:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp

The casting.yaml.lock pins every Docker image to its content digest, ensuring reproducible deployments. Judges can run foundryctl cast -f casting.yaml --locked to reproduce the exact SigNoz environment used during development.

Stack components

ServiceImagePurpose
ClickHouseclickhouse/clickhouse-server:24.12Time-series database storing all traces, metrics, and logs
OTel Collectorsignoz/signoz-otel-collector:0.119.3Receives OTLP from GlassPay and writes to ClickHouse
Query Servicesignoz/query-service:0.81.0SigNoz backend: API for dashboards, alerts, and queries
Frontendsignoz/frontend:0.81.0SigNoz Web UI at port 3301
MCP Serversignoz/mcp-server:latestAI-agent observability: expose SigNoz tools to your agent

The self-hosted stack requires 4 GB RAM, 2 CPU cores, and 20 GB disk for ClickHouse data. For production, use SigNoz Cloud at signoz.io — the same OTLP exporter configuration works with just a different endpoint and ingestion key.

API Reference

The server is one Hono process. The dashboard API lives under /api; the MCP endpoint, OAuth lane, x402 facilitator and demo surfaces sit at the root.

Auth lanes (every /api route)

  • Admin (Authorization: Bearer <GLASSPAY_ADMIN_TOKEN>): full access, server-side scripts only, never shipped to a browser.
  • Privy (Authorization: Bearer <Privy access token>): verified offline against the app JWKS; every route scoped to the authenticated user.

Dashboard API (/api)

Method · PathPurpose
POST /onboardRegister the embedded wallet + its 7702 auth + onboard proof
POST /cards/prepareCompile caveats, mint the agent key, return the unsigned delegation
POST /cards/finalizeAttach the browser signature, persist the card, return its URL
POST /cards/compileVenice NL → draft CardTerms (never issues)
GET /cardsList the user's cards
GET /cards/:idCard detail + charge ledger
GET /treeThe card → sub-card tree
GET /cards/:id/urlReveal the card URL
POST /cards/:id/rotateRotate the card secret (old URL dies)
GET /cards/:id/fiatThe linked test-mode Visa (owner view)
POST /cards/:id/freeze · /unfreezeReversible server-side pause / resume
POST /cards/:id/revoke/prepare · /finalizeClient-signed on-chain revoke (sub-cards die server-side)
POST /nuke/prepare · /finalizeClient-signed cascade nuke of every card
DELETE /cards/:idBookkeeping removal of a dead card + its subtree
GET /oauth/request · POST /oauth/approve · /denyThe card-picker consent backend

OAuth 2.1 (self-hosted authorization server)

Public clients, PKCE S256, auth-code + rotating refresh, dynamic client registration. Tokens are opaque (glsp_at_ access, glsp_rt_ refresh), audience-pinned, and die with the card.

EndpointSpec
GET /.well-known/oauth-protected-resource/mcpRFC 9728 protected-resource metadata
GET /.well-known/oauth-authorization-serverRFC 8414 AS metadata
POST /registerRFC 7591 dynamic client registration
GET /authorizeValidates, then 302s to the dashboard card-picker
POST /tokenauthorization_code + refresh_token grants, PKCE S256
POST /revokeRFC 7009 revocation (kills the whole token family)

MCP, facilitator + demo

EndpointPurpose
ALL /c/:secret/mcpLane A: secret in the path
ALL /mcpLane B (bearer) + Lane C (OAuth token)
GET /supported · POST /verify · /settleThe ERC-7710 x402 facilitator (under /facilitator)
GET /demo/premium-datax402-protected demo seller (0.01 USDC)
GET /shop/products · POST /shop/checkoutThe demo merchant API
GET /healthLiveness + engine version

Self-Hosting

A Bun monorepo, three packages: engine (the pure card engine), server (Hono: REST + MCP + facilitator + Stripe webhook + demo shop) and dashboard (Next.js). Real money moves on Base mainnet, so use small budgets.

bun install
cp .env.example .env          # then set the two required vars below

bun dev                       # server on :4070
bun run --cwd packages/dashboard dev   # dashboard on :4071

Required environment

VarPurpose
GLASSPAY_MASTER_KEY32-byte hex key; encrypts agent keys + card secrets at rest
GLASSPAY_ADMIN_TOKENOps bearer token for the management API (server-side only)
GLASSPAY_PRIVY_APP_IDDashboard lane: enables per-user Privy auth against the app JWKS

Common optional environment

VarPurpose
GLASSPAY_PUBLIC_MCP_BASEPublic origin for card URLs (also arms the MCP Host allowlist)
GLASSPAY_CORS_ORIGINSComma-separated allowed origins for the API + shop
STRIPE_SECRET_KEYStripe TEST-mode key (sk_test_/rk_test_ only); enables the fiat leg
GLASSPAY_STRIPE_WEBHOOK_SECRETReal-time auth webhook secret; unset = fiat leg disabled (503)
GLASSPAY_FIAT_SETTLEMENT1 = approved Visa charges settle on-chain as real USDC
VENICE_API_KEY · VENICE_MODELEnables /cards/compile; pin the model id
GLASSPAY_DASHBOARD_BASEDashboard origin hosting the OAuth consent page
GLASSPAY_RPC_URL · NEXT_PUBLIC_BASE_RPCBase RPC for server + client reads (default mainnet.base.org)
GLASSPAY_DB_PATHSQLite path (default .dev/glasspay.sqlite)
GLASSPAY_ALLOWED_HOSTSExtra Host headers accepted on the MCP endpoint (e.g. a platform fallback domain)
BASESCAN_API_KEYVerified-contract labels from Basescan when resolving compiled drafts
GLASSPAY_TRUST_PROXY_HOPSTrusted proxy hops for client-IP rate limiting (default 1 = Railway edge)
GLASSPAY_MCP_RATE_LIMIT · GLASSPAY_MCP_BAD_SECRET_LIMITPer-card and per-IP-bad-secret request ceilings per minute (240 / 30)
GLASSPAY_OAUTH_ACCESS_TTL · GLASSPAY_OAUTH_REFRESH_TTLOAuth access / refresh token lifetimes in seconds (3600 / 2592000)
GLASSPAY_OAUTH_REDIRECT_HOSTSIf set, restricts OAuth https redirect-URI hosts to this allowlist (recommended in prod)

Contracts (Base mainnet · chain 8453)

ContractAddress
DelegationManager0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3
Stateless7702 delegator impl0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B
USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913

The Cook Off

GlassPay was built for the MetaMask Smart Accounts Kit × 1Shot API × Venice AI Dev Cook Off. The hard gate, Smart Accounts Kit in the main flow, is the product itself: every card is a SAK delegation, signed by a Privy-provisioned embedded smart account, and every spend redeems it on-chain.

TrackWhat GlassPay does
x402 + ERC-7710paid_fetch pays HTTP 402 through the card's 7710 delegation; real x402 v2 on Base mainnet
Best Agent experienceOne URL is the whole integration; typed refusals; an OAuth lane for consent UX
Agent-to-agentissue_subcard redelegates narrower authority; revoke + nuke kill whole subtrees in one signature
Venice AIThe issue modal compiles plain language into signed card terms (model names, registry resolves)
1Shot RelayerEvery redemption rides the 1Shot Public Relayer, gasless, fees in USDC

GlassPay uses programmatic ERC-7710 Delegations, not ERC-7715 Advanced Permissions: the 7710 caveat set is richer than the 7715 grant catalog allows, so there is no wallet_requestExecutionPermissions path.

Where in the code

Direct links into the public repo for each track, following the MetaMask DevRel submission guideline.


Ready to issue one? Open the dashboard, sign in, and your first card takes about a minute.