---
name: urlgate
description: Create and sell pay-to-open short links on URLGate, open paid links, and manage everything a URLGate user can (unlocks, receipts, analytics, link options, membership, points) over REST or MCP, with x402 v2 USDC payments.
---

# URLGate

Everything in the web app is also available over REST (https://urlgate.xyz/openapi.json) and MCP (https://urlgate.xyz/mcp, tool names below).

## Start here (MCP: get_started)
`GET https://urlgate.xyz/api/v2/agents/get-started` (or the free MCP tool `get_started`, alias `pricing_and_limits`) returns JSON: pricing, your tier and today's free quota (`limit`, `used`, `remaining`, `resetsAt`) when you send credentials, and `nextSteps`. Human version: https://urlgate.xyz/agents.

## Creating links: three paths
1. Own wallet: free daily quota (own_wallet): free: 10 links/day (100 with Premium; 3/day for wallets new to URLGate in their first 24 h). Then 0.01 USDC per link. Earnings: your wallet.
2. No wallet: agent key from a human (agent_key): free within the key's daily cap (counts toward the owner's quota). Then 0.01 USDC per link. Earnings: the key owner's wallet.
3. Anonymous or over quota: pay per link (pay_per_link): 0.01 USDC per link via x402 (one signature) Earnings: the paying wallet (or your signed-in wallet).
Every quota or limit error carries machine-readable `nextSteps` (`action`: sign_in | use_agent_key | pay_0.01 | upgrade | wait). Over quota you get `402` with `code` (free_quota_exhausted, new_wallet_quota_exhausted, agent_key_cap_reached, free_global_ceiling, free_creation_paused, paid_only, anonymous) and `freeQuota`; paying the fee then works as usual. `429 rate_limited` (per wallet, agent key, IP) has `retryAfterSeconds`; `409 duplicate_link` returns the existing link; `403 domain_blocked`; `503 creation_paused` while an administrator has paused all creation. Live fees and limits: `GET https://urlgate.xyz/api/v2/config` (MCP `site_config`).
Agent keys (`ugk_…`, from a human's dashboard → Settings → Agent keys, or `agent_keys` / `POST https://urlgate.xyz/api/v2/me/agent-keys`): send as `Authorization: Bearer ugk_…` (MCP: `token`). They only create links; the links belong to the key owner.

## Check your client first (MCP: payment_compat_check)
Free and verify-only: nothing is ever settled. `POST https://urlgate.xyz/api/v2/payments/compat-check` (or the MCP tool `payment_compat_check`) → `402` with a two-leg offer (tiny amounts, 30 s `validBefore`); sign BOTH legs, call again → `{ verdict }`: `ok` | `fee_leg_missing` | `main_leg_missing` | `wrong_network` | `bad_signature` | `amount_mismatch` (also `requirement_mismatch`, `payer_mismatch`, `expired`, `insufficient_funds`, `facilitator_unavailable`). Signatures sent to the check can never pay for anything.

The preview testnet test link REQUIRES a wallet with Base Sepolia testnet + test USDC.
If your wallet can't use testnet, follow the production path: run payment_compat_check on production (verify-only, no funds move) and, if you meet the two-signature requirements, proceed with the real paid tools.
Testnet path:
1. Use a wallet on Base Sepolia (eip155:84532) holding test USDC (e.g. faucet.circle.com). Only signatures are needed, no gas.
2. Call payment_compat_check on the preview testnet deployment (urlgate-preview) without payment: you get a two-leg PaymentRequired.
3. Sign BOTH legs (an accepts[] entry + the extensions["platform-fee"].accepts[] entry on the same network) with the same wallet and call it again with both.
4. verdict "ok" = your client can pay URLGate. Nothing is ever settled.
Production path:
1. Call payment_compat_check on production (https://urlgate.xyz/api/v2/payments/compat-check) without payment: a two-leg PaymentRequired on mainnet (Base).
2. Sign BOTH legs and call it again with both signatures. Verify-only: no funds move (the wallet only needs to hold the 0.001 USDC test amount for verification).
3. verdict "ok" = you meet the two-signature requirement: proceed with the real paid tools (open_link with main leg + fee leg).
4. Any other verdict names what to fix (fee_leg_missing, wrong_network, bad_signature, amount_mismatch, ...); fix it and re-run the check before paying.

## Paid opens need TWO signatures (main leg + platform-fee leg)
x402 `exact` pays one recipient per signature, so opening a paid link is two EIP-3009 authorizations, from the same wallet, in ONE request:
1. Pick one entry of accepts[] (the main leg, to the creator) on a network where you hold USDC.
2. Pick the entry of extensions["platform-fee"].accepts[] on the SAME network (the fee leg, to URLGate).
3. Sign an EIP-3009 TransferWithAuthorization for EACH entry with the SAME wallet (two signatures, two different nonces).
4. Send both in ONE retry. HTTP: PAYMENT-SIGNATURE (main) + PAYMENT-SIGNATURE-FEE (fee), each a base64 x402 v2 PaymentPayload. MCP: params._meta["x402/payment"] (main) + params._meta["x402/payment-fee"] (fee) as PaymentPayload objects, or the tool arguments paymentSignature + paymentSignatureFee as base64 strings.
5. URLGate verifies and reserves BOTH legs before settling either. Missing fee leg: refused with code fee_leg_missing and nothing is charged.

Only the main leg? Refused with `fee_leg_missing` (`missing: ["platform-fee"]`, `howToFix`) BEFORE anything is verified or settled: nothing is charged, and the same main-leg signature can be resent together with the fee leg. Paying for link creation (`create_link`, when not free) and buying membership (`buy_membership`) are ONE signature (no fee leg).

The two legs are two on-chain transfers and can't settle atomically. Before settling, the payer's USDC balance must cover BOTH (else `insufficient_funds`, `charged: false`, `needed` / `balance`; nothing settled). If one leg still settles and the other fails, the wallet has a PARTIAL PAYMENT for that link: every later request for the link from that wallet (payment payer, Bearer, SIGN-IN-WITH-X) gets `402` `code: "partial_payment"` with `partial: { paid: { leg, amount, tx, txUrl, network }, owed: { leg, amount }, expiresAt }` and `accepts[]` holding ONLY the owed leg. Sign that one entry and send it alone (creator owed: PAYMENT-SIGNATURE / _meta["x402/payment"]; fee owed: PAYMENT-SIGNATURE-FEE / _meta["x402/payment-fee"], or the main slot). Never re-sign the settled leg; it is never charged twice. Promo codes don't change an owed amount.

Worked example (a 0.05 USDC link at 0.3%: main 49850 units to the creator, fee 150 units to URLGate):
```
GET https://urlgate.xyz/Gp1qJx  (Accept: application/json)
→ 402, PAYMENT-REQUIRED = base64({ accepts: [{ network: "eip155:8453", amount: "49850", payTo: "0xCreator…", … }],
                                   extensions: { "platform-fee": { accepts: [{ network: "eip155:8453", amount: "150", payTo: "0xURLGate…", … }] } } })
sign main = EIP-3009(to 0xCreator…, value 49850, nonce N1)   sign fee = EIP-3009(to 0xURLGate…, value 150, nonce N2)
HTTP: GET https://urlgate.xyz/Gp1qJx  PAYMENT-SIGNATURE: base64(PaymentPayload main)  PAYMENT-SIGNATURE-FEE: base64(PaymentPayload fee)
MCP:  {"method":"tools/call","params":{"name":"open_link","arguments":{"code":"Gp1qJx"},
       "_meta":{"x402/payment":{…PaymentPayload main…},"x402/payment-fee":{…PaymentPayload fee…}}}}
→ 200 { destination, receiptUrl } + PAYMENT-RESPONSE (MCP: result._meta["x402/payment-response"])
```

## Open a paid link (MCP: open_link)
1. `GET https://urlgate.xyz/<code>` with `Accept: application/json` → `402`. Decode the base64 `PAYMENT-REQUIRED` header (x402 v2).
2. Pick an `accepts[]` entry (scheme `exact`, a CAIP-2 network you hold USDC on) AND the `extensions["platform-fee"].accepts[]` entry on the same network, sign both EIP-3009 authorizations, retry with `PAYMENT-SIGNATURE` + `PAYMENT-SIGNATURE-FEE` (see above).
3. `200 { destination, receiptUrl }`, with the settlement in the `PAYMENT-RESPONSE` header.
Already paid? Send a `SIGN-IN-WITH-X` header (or a Bearer token for the buying wallet) and you get back in without paying again. Promo code: `?promo=CODE` or `X-Promo-Code`.
Browser preview: `https://urlgate.xyz/<code>?preview=1` never auto-forwards (paywall, or the buyer's receipt) and isn't counted; JSON requests ignore it.
Taken down? `410` with `status` (paused, expired, sold_out, removed_by_seller, removed_by_admin) and `reason` (dmca, legal, phishing_malware, tos_violation, other). Check first with `link_status`.

## After paying
- My unlocks: `GET https://urlgate.xyz/api/v2/me/unlocks` (my_unlocks); taken-down links stay listed as unavailable.
- Receipt: `GET https://urlgate.xyz/api/v2/receipts/<id>` (get_receipt); human page `https://urlgate.xyz/r/<id>`.

## Sell (sign in first)
- Create: `POST https://urlgate.xyz/api/urlgate/agent/create` `{ "url", "price", "wallet"? }` (MCP create_link). Signed in (Bearer / SIGN-IN-WITH-X) or with an agent key: free within your daily quota (10/day, 100 with Premium) and the link is yours. Anonymous or over quota: the 0.01 USDC creation fee via x402 and the link belongs to the paying wallet. A browser cookie alone is not an API credential (`POST https://urlgate.xyz/api/urlgate/create` follows the same rules except for the website's own create form, which is free). The `wallet` receives 99.7% of what buyers pay. `price` is a plain decimal USDC amount, minimum 0.01 (max 6 decimals).
- My links / one link: `GET /api/v2/me/links`, `GET /api/v2/links/<code>` (my_links, get_my_link).
- Options: `PATCH /api/v2/links/<code>` (update_link): price, title, description, afterPay (receipt|redirect), networks; premium: receiptMessage, expiresAt, maxUses, accessTtlSeconds, destination after the first sale.
- Status: `POST /api/v2/links/<code>/{pause,resume,remove,restore}` (set_link_status).
- Analytics, sales, audit: `/api/v2/links/<code>/analytics|payments|audit`, `/api/v2/me/analytics` (link_analytics, link_payments, link_audit, my_analytics).
- Allowlist / promo codes (premium): `/api/v2/links/<code>/grants` (link_grants). QR: `/api/v2/links/<code>/qr.svg` (link_qr).
- Premium-only options answer `403 { code: "premium_required", feature, purchaseUrl }`.

## Membership (MCP: membership_status, buy_membership, redeem_premium_code)
`GET https://urlgate.xyz/api/v2/membership`; buy 30 days (5.00 USDC) with `POST https://urlgate.xyz/api/v2/membership/purchase` (x402, same flow as opening a link). Nothing renews automatically; renewing early stacks the days.
Premium code (signed in): `POST https://urlgate.xyz/api/v2/membership/redeem {"code":"…"}` adds the code's cycles (30 days each) on top of your current period; once per wallet per code. Unknown, expired, withdrawn and used-up codes all answer `400 invalid_code`; `409 already_redeemed`; `429` when retried too fast.

## Points (MCP: my_points, points_leaderboard, points_leaderboard_me, points_rules, set_points_prefs)
Sale points stay pending for 7 days, then a review confirms or removes them. `GET /api/v2/points/me` shows confirmed vs pending and why anything was not eligible.
Leaderboard: `GET /api/v2/points/leaderboard?limit=20&page=2` (or `offset`; top 100 only, cached 60s) ranks by confirmed points only (ties: earliest earner); each row also shows pending: points whose review is still open (pending or held, not voided, confirmed or expired), which never count toward rank. Wallets are shortened (0x1234…abcd); opted-out and banned wallets are not listed. Your own rank: `GET /api/v2/points/leaderboard/me` (signed in) → `{ rank, finalized, pending }`, rank null when not listed.

## Sign in (MCP: sign_in)
`GET https://urlgate.xyz/api/v2/auth/nonce?address=0x…` → sign `message` (EIP-191) → `POST https://urlgate.xyz/api/v2/auth/verify {message, signature, kind:"agent"}` → `Authorization: Bearer <token>` (valid 24h). Sessions: `GET /api/v2/auth/sessions`, `POST /api/v2/auth/revoke` (`{"scope":"all"}` if a token leaked).

## Settings (MCP: my_preferences)
`GET https://urlgate.xyz/api/v2/me/preferences` → `{ preferences: { theme } }`; `PATCH` with `{"theme":"green"|"blue"|"system"}` (or `null`) saves the colour scheme the website uses for your wallet (default `blue`). Own wallet only.

## MCP
`POST https://urlgate.xyz/mcp` (Streamable HTTP, JSON). Send `Authorization: Bearer <token>` or `SIGN-IN-WITH-X` on the connection, or pass `token` in a tool's arguments. Paid tools (open_link, buy_membership, create_link) follow the x402 MCP transport: without payment they return `isError: true` with the `PaymentRequired` in `structuredContent` (and as JSON in `content[0].text`); call again with the PaymentPayload in `params._meta["x402/payment"]` (open_link also `params._meta["x402/payment-fee"]`), or the `paymentSignature` / `paymentSignatureFee` arguments (base64). Settlement: `result._meta["x402/payment-response"]`. Errors: `structuredContent.error` = `"<code>: <message>"`, details in `result._meta["urlgate/payment-error"]`.

Libraries: `@x402/fetch` + `@x402/evm` handle the payment steps automatically.

## Agents vs humans
Creators see whether each sale came from an agent or a human (`payerKind`: agent | human | unknown on `link_payments`; per-link split `totals.payerKinds` on `link_analytics`). It is estimated from how you paid (paywall page, raw x402 header, MCP, token type, client family), never from what you claim. To be counted as a **verified** agent, include an ERC-8004 identity in the x402 v2 payment payload: `extensions["8004-reputation"].info.client.agentIdentity = { agentRegistry: "eip155:<chainId>:<IdentityRegistry>", agentId }`; URLGate checks on-chain that the agent's `agentWallet` (or owner) is the paying wallet. A self-declared `X-URLGate-Client: agent` header is a hint only.

## All MCP tools
- `open_link`: Open a paid link. Without a payment signature you get the x402 v2 payment requirements; send them back signed to get the destination. Already paid (signed in as the buyer)? You get the destination directly. Taken-down links return status 410 with a machine-readable reason. PAYMENT TAKES TWO SIGNATURES: (1) call without payment → isError result whose structuredContent is the x402 PaymentRequired; (2) sign one accepts[] entry (main leg, to the creator) AND the extensions["platform-fee"].accepts[] entry on the same network (0.3% fee leg, to URLGate), same wallet; (3) call again with both: params._meta["x402/payment"] + params._meta["x402/payment-fee"] (PaymentPayload objects) or the arguments paymentSignature + paymentSignatureFee (base64). Only the main leg → refused with fee_leg_missing before anything is charged. Balance must cover BOTH legs (else insufficient_funds, charged:false). If an earlier payment settled only one leg, the result has code partial_payment, partial { paid, owed } and accepts[] with ONLY the owed leg: sign that one and send it alone (fee owed: x402/payment-fee or paymentSignatureFee). Settlement comes back in result._meta["x402/payment-response"]. Test your client for free first with payment_compat_check. The preview testnet test link REQUIRES a wallet with Base Sepolia testnet + test USDC. If your wallet can't use testnet, follow the production path: run payment_compat_check on production (verify-only, no funds move) and, if you meet the two-signature requirements, proceed with the real paid tools.
- `get_link_info`: Public metadata for a link: price, title, description, options, status. Never reveals the destination.
- `link_status`: Availability of a link: active / paused / expired / sold_out / removed_by_seller / removed_by_admin, with the reason code and public note.
- `report_link`: Report a link to URLGate moderators (scam / phishing, malware, illegal content, copyright, spam, child sexual abuse or exploitation). One report per reporter per link.
- `my_unlocks`: Links you paid for, newest first, with access state (open / expired / unavailable + reason), receipt and tx links.
- `get_receipt`: A receipt by id: paid at, amount, fee, explorer links, permanent URL.
- `payment_compat_check`: FREE, verify-only check that your x402 client can pay URLGate before you spend anything. Call without payment → isError result with a two-leg PaymentRequired (main accepts[] entry + extensions["platform-fee"] entry; tiny amounts, 30 s validBefore). Sign BOTH with the same wallet and call again with _meta["x402/payment"] + _meta["x402/payment-fee"] (or paymentSignature + paymentSignatureFee). Both legs are verified, NEVER settled; you get verdict ok | fee_leg_missing | main_leg_missing | wrong_network | bad_signature | amount_mismatch (or requirement_mismatch, payer_mismatch, expired, insufficient_funds, facilitator_unavailable). Signatures sent here can never pay for anything. The preview testnet test link REQUIRES a wallet with Base Sepolia testnet + test USDC. If your wallet can't use testnet, follow the production path: run payment_compat_check on production (verify-only, no funds move) and, if you meet the two-signature requirements, proceed with the real paid tools. Testnet path: 1. wallet on Base Sepolia with test USDC; 2. payment_compat_check on the preview deployment; 3. sign both legs, call again; 4. verdict ok = ready. Production path: 1. payment_compat_check on production (verify-only, no funds move); 2. sign both legs, call again; 3. verdict ok = proceed with the real paid tools.
- `create_link`: Create a paid short link. Signed in (sign_in token, Bearer or SIGN-IN-WITH-X) or with an agent key (token = ugk_…): FREE within your daily quota (10 a day, 100 on Premium; see get_started), the link and its earnings are your wallet's (an agent key's owner). Anonymous, over quota, or free creation paused: 0.01 USDC via x402: the call returns isError with the PaymentRequired and a code (anonymous, free_quota_exhausted, …) plus nextSteps in _meta["urlgate/payment-error"]; call again with the payment. Limits: 429 rate_limited, 409 duplicate_link, 403 domain_blocked. Payment is ONE signature (no platform-fee leg): sign an accepts[] entry and call again with params._meta["x402/payment"] (PaymentPayload) or paymentSignature (base64). Settlement comes back in result._meta["x402/payment-response"].
- `get_started`: FREE, call this first. Returns JSON: pricing and limits, your tier and today's free-link quota (limit / used / remaining / resetsAt) when signed in or using an agent key, the three ways to create links (own wallet = free daily quota, earnings to you; agent key from a human = free within the key cap, earnings to the owner; anonymous or over quota = 0.01 USDC via x402), and nextSteps (sign_in, use_agent_key, pay_0.01, upgrade, create_link, compat_check).
- `pricing_and_limits`: FREE. Alias of get_started: creation fee, free daily quota (Free / Premium / new wallets), global ceiling, agent-key limits, per-minute limits, Premium perks, and your remaining free links today when signed in or using an agent key.
- `my_links`: Links you created: destination, options, status (with reason and when it changed), clicks, 402 views, sales, free unlocks, unique payers, gross, platform fees, earnings, conversion, failed payments, agent vs human sales, last sale and abuse report count.
- `get_my_link`: Owner view of one of your links, including destination, options and which premium options you can use.
- `update_link`: Edit one of your links. Free: price, title, description, afterPay, networks. Premium: receiptMessage, expiresAt, maxUses, accessTtlSeconds, destinationUrl after the first sale. null clears an option.
- `set_link_status`: Pause, resume, remove or restore one of your links. Buyers who already paid keep access while paused.
- `link_analytics`: Analytics for one of your links, including the agent vs human split of sales (payerKinds) per day and in total (range 7d / 30d; 90d / all and referrer/country/buyer breakdowns need premium).
- `my_analytics`: Your totals and daily earnings across all your links.
- `link_payments`: Payments for one of your links (buyer, amount, fee, tx links, payerKind agent/human/unknown). Optional payerKind filter.
- `link_audit`: Audit trail for one of your links: 402 shown, paid, re-opened, edits, status changes. IP addresses are truncated.
- `link_grants`: Manage the allowlist / promo codes of one of your links (premium). action=list|create|revoke.
- `link_qr`: QR code for a link as SVG markup.
- `membership_status`: Premium plan (price, period, features) and your membership: tier, state, period end, days left, renewal reminder.
- `buy_membership`: Buy (or extend by) 30 days of Premium (5.00 USDC). Call without a signature to get the x402 requirements; nothing renews automatically. Credited to the signed-in wallet, else the payer. Payment is ONE signature (no platform-fee leg): sign an accepts[] entry and call again with params._meta["x402/payment"] (PaymentPayload) or paymentSignature (base64). Settlement comes back in result._meta["x402/payment-response"].
- `redeem_premium_code`: Redeem a URLGate premium code for the signed-in wallet. Adds the code’s cycles (30 days each) on top of your current period. Each wallet can use a code once; invalid, expired, withdrawn and used-up codes all return invalid_code.
- `dismiss_renewal_reminder`: Dismiss the "Premium ends soon" reminder for the current period.
- `my_points`: Your points: confirmed vs pending (7-day review), items with state and reason, not-eligible events, rank, adjustments.
- `set_points_prefs`: Hide or show your wallet on the public leaderboard (points keep accruing).
- `points_leaderboard`: Public top-100 leaderboard ranked by confirmed (final) points only, ties to the earliest earner. Each row also shows pending: points whose review is still open (not voided, confirmed or expired), never used for rank. Banned wallets are excluded. Wallets are shortened. Page with limit (max 100) and offset or page.
- `points_leaderboard_me`: Your rank on the public leaderboard plus confirmed and pending points. rank is null when you have no confirmed points, opted out, or are banned (reason says which).
- `points_rules`: How points are earned, the caps, and every not-eligible reason.
- `site_config`: FREE. The fees, prices, quotas, periods and limits in force right now, as JSON (the values every URLGate page and doc is rendered from). Personal quota usage: get_started.
- `verify_ledger`: Verify the hash chain of the points ledger.
- `sign_in`: Sign in with a wallet. Step 1: {address} → message to sign (EIP-191). Step 2: {message, signature} → token; pass it as Authorization: Bearer on the MCP connection (or as `token` on each tool call).
- `whoami`: The wallet this connection is signed in as, with tier and membership.
- `sessions`: List your sessions/tokens (action=list) or revoke them (action=revoke with scope current|all or a jti).
- `agent_keys`: Manage agent keys for bots that have no wallet (signed in as the owner). action=list (default) | create (name, dailyCap, expiresInDays, networks; the key is returned once) | revoke (id). A key can only create links; they belong to your wallet.
- `my_preferences`: Your display preferences. Without arguments: read them. With theme (green | blue | system, or null to reset): save the colour scheme the URLGate website uses for your wallet.
- `list_networks`: Payment networks this deployment accepts right now (CAIP-2), the USDC contract on each, which is the default, and the mode (mainnet on production, testnet on preview). A link's networks must be a non-empty subset of these.
- `open_link_instructions`: How to pay for and open a link over HTTP x402 v2 (returns the exact URL and steps).
