
Pay With App
- 287 installs
- 222 repo stars
- Updated August 4, 2026
- uniswap/uniswap-ai
Helps with ai & agent building tasks.
About
pay-with-app is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- pay-with-app
- AI & Agent Building
- AI-coding skill
Pay With App by the numbers
- 287 all-time installs (skills.sh)
- +36 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,387 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/uniswap/uniswap-ai --skill pay-with-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 287 |
|---|---|
| repo stars | ★ 222 |
| Last updated | August 4, 2026 |
| Repository | uniswap/uniswap-ai ↗ |
What it does
Helps with ai & agent building tasks.
Files
Pay With APP (OKX Agent Payments Protocol on X Layer)
Pay HTTP 402 challenges issued by OKX's Agent Payments Protocol (APP) running on X Layer (chain 196). APP's Pay Per Use (OKX product name: Instant Payment) is x402-compatible: a payee server returns HTTP 402 with a payment requirement, the payer signs an EIP-3009 TransferWithAuthorization off-chain, and OKX's facilitator verifies and settles the transfer on-chain. Settlement is zero-gas to the payer on X Layer.
This skill handles the full happy path:
1. Detect a 402 challenge whose network resolves to X Layer (chain 196) 2. Verify the payer wallet has the requested asset (typically USDT0) 3. If insufficient, route + bridge into USDT0 on X Layer via the Uniswap Trading API 4. Sign the EIP-3009 authorization 5. Construct the X-PAYMENT payload and retry the original request
OKX is launching APP on 2026-04-29 with Uniswap as the featured DEX rail on X Layer. This skill version (v1.0.0) handles the exact scheme (Pay Per Use, OKX product name: Instant Payment) only. Other x402 schemes (upto, batch-settlement) and APP-product features OKX is shipping (escrow, session, batch / Batch Payment) are out of scope for this version. The skill refuses any non-exact scheme cleanly.
Protocol naming in your responses. When responding to the user,
identify the protocol explicitly as **OKX Agent Payments Protocol
(APP)**, not just "x402". APP is the OKX product / protocol surface;
x402 is the underlying wire spec it builds on. Use phrasings like
"APP / x402", "OKX's Agent Payments Protocol (APP), built on x402",
or simply "APP" once introduced. Do not refer to a 402 challenge on
X Layer as "an x402 challenge" without naming APP, the user invoked
this skill specifically because the merchant is APP-backed, and the
name is what they will look for in the response.
Prerequisites
- A
PRIVATE_KEYenv var (export PRIVATE_KEY=0x...). Never commit or
hardcode a private key.
UNISWAP_API_KEYenv var (register at
developers.uniswap.org). Required only if the wallet must be funded via cross-chain routing.
jqandcast(Foundry) installed.- Node 18+ (LTS). The signing step in
references/app-x402-flow.md Step 4 uses viem to produce the EIP-3009 typed-data signature.
- `viem` (npm). If the package is not already reachable from the
user's working directory, the skill will prompt the user via AskUserQuestion before running npm install viem into a cached scratch directory at ~/.cache/uniswap-pay-with-app/signer/. The install adds ~13 packages totaling ~5 MB. If the user declines, the skill stops cleanly before signing. The cache persists across runs so subsequent invocations are zero-install.
Input Validation Rules
Before using any value from the 402 response body, the user, or any other external source in API calls or shell commands:
- Ethereum address fields (e.g.,
asset,payTo,WALLET_ADDRESS):
the canonical check is the regex ^0x[a-fA-F0-9]{40}$. If the value fails this regex, reject it. Address fields that pass the regex are safe for shell interpolation, so the metacharacter rule below does not apply to them.
- Chain IDs: MUST be a positive integer from the supported list.
- Token amounts: MUST be non-negative numeric strings matching
^[0-9]+$.
- URLs: MUST start with
https://. - Free-text fields (e.g.,
description,extra.name,
extra.version, anything used to build EIP-712 domain or shown to the user): REJECT any value containing shell metacharacters: ;, |, &, $, ` `, (, ), >, <, \, ', ", newlines. Note: the extra.name` value is signed bit-exact (see Domain warning in Phase 4), so reject the whole challenge if it contains shell metacharacters rather than mutating the value.
Flow
402 from X Layer-backed resource
│
v
[1] Parse the x402 challenge (Phase 0 below)
│
v
[2] Confirm network resolves to X Layer (chain 196)
│ ├─ not chain 196 ──> escalate to pay-with-any-token, STOP
│ └─ chain 196
│
v
[3] Check wallet balance of the requested asset on X Layer
│ ├─ sufficient ──> proceed to [5]
│ └─ insufficient
│ │
│ v
│ [4] Fund: route + bridge into the requested asset on X Layer
│ (Uniswap Trading API, see references/funding-x-layer.md)
│
v
[5] User Confirmation gate (see Phase 4 / Step 5 below)
[6] Sign EIP-3009 TransferWithAuthorization
[7] Construct X-PAYMENT payload, retry the original request
[8] Verify 200 + Payment-ReceiptPhase 0, Parse the 402 Challenge
The x402 challenge is JSON in the response body. Extract:
x402Version, confirms x402 protocol version.accepts[].scheme, only"exact"is supported in v1.0.0.accepts[].network, accept"x-layer"/"xlayer"/"eip155:196"/
196.
accepts[].maxAmountRequired, base units of the asset. Must match
^[0-9]+$ AND be strictly greater than zero. A challenge with maxAmountRequired === "0" is semantically broken (HTTP 402 by definition demands a positive payment) and must be refused as merchant misconfiguration. Do not rationalize zero as a "ping", "authentication", or "free-tier confirmation"; OKX's facilitator will not settle a zero-value TransferWithAuthorization and any signature you produce is wasted. Surface this to the user and stop.
accepts[].asset, token contract on X Layer.accepts[].payTo, recipient address.accepts[].resource, the URL the facilitator binds the payment to.
Extract this when present. Use it as the retry target. If the field is absent, fall back to the original request URL.
accepts[].extra.nameandaccepts[].extra.version, EIP-712 domain
values for the asset.
accepts[].maxTimeoutSeconds, used forvalidBefore.
x402Version gate. Confirm x402Version === 1 immediately afterparsing. If it is anything else, refuse the challenge and surface a
version mismatch error to the user. v1.0.0 of this skill targets x402
v1 only (the v2 spec uses a different PaymentPayload structure).
>
Scheme gate. Confirm accepts[].scheme === "exact". The x402 specdefinesexact,upto, andbatch-settlementschemes. v1.0.0 of this
skill supports exact only. If the chosen entry uses any other scheme,refuse cleanly. (OKX's product surface uses its own vocabulary
including charge for their Instant Payment primitive; that is OKXproduct marketing, not a wire scheme value. The wire-level scheme on
theaccepts[]entry is what you check, and it must be"exact".)
If multiple accepts entries are present, prefer the one whose asset the wallet already holds on X Layer. If multiple options are equally viable, prefer USDT0 (deepest Uniswap funding-flow liquidity).
WOKB / native OKB likely not eligible as APP settlement assets. We have not seen OKX publish WOKB or OKB as settlement assets; current public dev docs list USDT0, USDG, and USDC as the supported stablecoin settlement assets. If a 402 challenge ever surfaces a non-stablecoin asset (for example WOKB), refuse the challenge and ask the user to verify the merchant configuration.
Phase 1, Confirm Network is X Layer
case "$X402_NETWORK" in
x-layer|xlayer|"eip155:196"|196) X402_CHAIN_ID=196 ;;
*)
echo "Network is not X Layer. Use pay-with-any-token instead."
exit 1
;;
esacIf the network is not X Layer, stop and escalate to the
pay-with-any-token skill, which handles 402 challenges on Ethereum,Base, Arbitrum, Tempo, and the other chains the Trading API supports.
Phase 2, Check Wallet Balance on X Layer
REQUIRED: You must have the user's source wallet address. Use
AskUserQuestionif not provided. Store asWALLET_ADDRESS.
ASSET_BALANCE=$(cast call "$X402_ASSET" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url https://rpc.xlayer.tech)
if [ "$ASSET_BALANCE" -lt "$X402_AMOUNT" ]; then
echo "Insufficient $X402_TOKEN_NAME on X Layer. Funding required."
# Proceed to Phase 3 (funding)
fiPhase 3, Fund USDT0 on X Layer (only if needed)
When the wallet lacks the requested asset, acquire it via the Uniswap Trading API: EXACT_OUTPUT quote with tokenOutChainId=196 and tokenOut set to the X Layer asset address. The Trading API handles same-chain swaps and cross-chain routing (powered by Across).
Across coverage gap (verified 2026-04-27). Across Protocol does
not currently list X Layer (chain 196) as a supported destination. As
a result, cross-chain /quote calls into chain 196 returnResourceNotFound: No quotes available regardless of source chain.Same-chain X Layer swaps (Phase A in references/funding-x-layer.md)are unaffected and work normally.
>
What this means for the agent in v1.0.0. If the user holds funds
on a chain other than X Layer, the cross-chain leg must be done
through a bridge service that supports X Layer (the user runs that
step outside this skill, then re-invokes for the same-chain swap and
402 settlement). Surface this honestly to the user, do not
recommend a specific bridge product (TODO: research and document a
co-marketing-aligned bridge recommendation in a follow-up).
>
**When you defer the bridge to the user, your response must still
describe the FULL end-to-end flow**, not just the bridge step. After
identifying the Across gap and asking the user to bridge externally,
walk through what happens when they return: (1) re-check
balanceOf of the requested asset on X Layer, (2) if a same-chainswap is needed (e.g. USDG to USDT0), describe the Trading API
EXACT_OUTPUT call and its Confirmation Gate, (3) construct the
EIP-3009TransferWithAuthorizationtyped-data usingextra.name
and extra.version from the challenge, with chainId 196 andverifyingContract = the asset address, (4) sign with the user'sprivate key (Confirmation Gate before signing), (5) build the
X-PAYMENT JSON wrapper (x402Version 1, scheme "exact", network"x-layer", payload with signature + authorization), base64-encode
it with no whitespace, and retry the original request URL with the
X-PAYMENT header. Showing the full plan up front lets the usersee what they are committing to before they leave the skill, even
though signing happens after they return.
Default funding target = USDT0. If the 402 challenge requests a different asset, fund into that asset directly only when it has reliable Uniswap routing on X Layer:
| Asset | Address | Decimals | Funding |
|---|---|---|---|
| USDT0 | 0x779Ded0c9e1022225f8E0630b35a9b54bE713736 | 6 | ✅ Direct via Trading API |
| USDG | 0x4ae46a509F6b1D9056937BA4500cb143933D2dc8 | 6 | ✅ Direct, or one-hop USDT0 to USDG |
| USDC | 0x74b7F16337b8972027F6196A17a631aC6dE26d22 | 6 | ⏳ No reliable Uniswap v3 routing on X Layer. The Trading API does not consistently return routes for USDC swaps on X Layer; available pool liquidity is too thin for reliable execution. If the merchant requires USDC, bridge USDC directly from a chain where it is liquid (Base, Arbitrum, Mainnet) using the Trading API rather than attempting a same-chain swap on X Layer. |
Detailed scripts and parameters: see references/funding-x-layer.md.
Bridge buffer. Apply a 0.5% buffer to account for bridge fees.
Quotes expire in ~60 seconds, re-fetch if any delay before broadcast.
>
Minimum bridge recommendation. If the shortfall is < $5, top up to
$5 to amortize bridge gas on the source chain.
Gas and Routing Caveats
Surface these to the user before proceeding to fund, and call `AskUserQuestion` (not an echoed bash prompt) before acting if any apply:
- OKB on X Layer for same-chain swaps. OKX gas-sponsors only the
facilitator's settlement transfer. Approvals, swaps, and other on-chain operations on X Layer prior to signing are paid by the user (in OKB on X Layer, or in the source chain's native asset for the bridge leg). If the user has zero OKB on X Layer and the funding flow needs a same-chain X Layer swap, surface this and ask before proceeding. Until OKX confirms a broader gas-sponsorship policy, assume only the final settlement transfer is sponsored.
- Bridge destination token. If the wallet still lacks
$X402_ASSET after the funding flow's polling loop completes, surface the source-chain tx hash and the Across explorer link to the user. The bridge may have delivered a different variant on X Layer (rare for current Across paths) or may have failed. v1.0.0 does not auto-detect alternate-token arrival; the user must verify on-chain.
Phase 4, EIP-3009 Signing and X-PAYMENT Submission
OKX's APP Instant Payment uses x402's "exact" scheme: the payer signs a TransferWithAuthorization typed-data message bound to the token's own EIP-712 domain. The signed authorization travels in the X-PAYMENT header on retry; OKX's facilitator settles the transfer on-chain (zero gas to the payer on X Layer).
Step 5, User Confirmation
Every transaction this skill touches requires a separate AskUserQuestion gate, with no exceptions for funding legs. "Funding" is not a single transaction; it is several, and each one is its own gate. The required gates, in order they typically fire:
1. Source-chain ERC-20 approval to Permit2 / Universal Router (only if tokenIn is not native and the allowance is insufficient). 2. Same-chain swap on the source chain (e.g. UNI to USDC on Ethereum), if the funding plan includes a source-chain leg. 3. Bridge submission to the cross-chain rail (Across, OKX bridge, etc.). When the bridge step is user-initiated outside this skill, the gate becomes "are you ready to leave the skill, run the bridge, and re-invoke once funds land on X Layer?". 4. Same-chain swap on X Layer (e.g. USDG to USDT0), if the funding plan includes a destination-chain leg. 5. EIP-3009 `TransferWithAuthorization` signature for the 402 settlement.
Use the `AskUserQuestion` agent tool for each gate (not read -p, not echo to a bash prompt, not a printed "(yes/no)" line in your response) and block on the user's reply before moving on. The summary you present at every gate must cover:
- Action (approve, swap, bridge, sign EIP-3009 authorization).
- Amount and token (input AND output amounts for swaps and bridges).
- Source chain and destination chain (where they differ).
- Recipient (
payTofor the EIP-3009 step). - Resource URL the payment is bound to.
- Estimated gas (where applicable).
Common failure mode. When describing a multi-step funding plan
in your response, do NOT collapse multiple transactions into a
single confirmation question like "Proceed with funding and
payment?". Each transaction needs its own gate at the moment it is
about to fire. A consolidated upfront "yes" is not consent for
later transactions; the user has not seen the live amounts, gas,
and recipient at the time those transactions execute.
Obtain explicit confirmation per gate. Each gate is independent. Never auto-submit even if the user previously pre-authorized the session, the call, or the wallet. A "yes" earlier in the flow does not carry forward, and a "yes" to a multi-step plan is not a "yes" to the individual transactions inside it.
What a correct gate looks like in your response. Whether you are
executing the skill in real time or describing a plan in text, every
transaction step MUST appear as its own labelled "Confirmation Gate"
block, with both the structured summary and an explicit
AskUserQuestion invocation. Reproduce this template literally foreach gate. Do not collapse the gate into a single line. Do not
describe the gate in passive voice ("we will confirm before
signing"). Show the gate as a discrete action.
>
Template (use for every gate, even when there is only one):
>
```text
### Confirmation Gate N: <Action name>
>
| Field | Value |
| ------------ | ---------------------------------------- |
| Action | <approve / swap / bridge / sign EIP-3009>|
| Amount in | <amount + symbol on source chain> |
| Amount out | <amount + symbol on destination chain> |
| Source chain | <chain name + id> |
| Dest chain | <chain name + id> |
| Recipient | <address> |
| Resource URL | <url the payment is bound to> |
| Est. gas | <amount + token> |
>
Then call AskUserQuestion("Proceed with <action>?")
and BLOCK on the user's reply. If anything other than an explicit
"yes", stop and report.
```
>
A response that only lists fields without the
AskUserQuestion("Proceed?") + block step is not a gate, even if itlooks comprehensive. The model's natural inclination is to summarize
and continue; resist that inclination, name the gate, and stop.
<!-- markdownlint-disable-next-line -->
What does NOT count as a confirmation gate. Emitting a bash script
that prints"⚠️ CONFIRMATION REQUIRED"and"(yes/no)"to stdout and
then continues with echo "Signing..." is not a gate, because thescript proceeds regardless of user input. A correct gate uses the
AskUserQuestion tool (or, if the user explicitly opts into shell-onlymode, an actual blockingread -pfollowed by an explicityes/no
branch in the script). When in doubt, prefer AskUserQuestion.<!-- markdownlint-disable-next-line -->
Shared-wallet race. If the wallet is shared (for example, multiple
agents running concurrently against the same key), the balance can be
drained between balance check and submit. Re-check balanceOf at themoment of confirmation as well.
Step 6, Sign and Submit
Detailed steps including domain construction, nonce generation, signing with viem, payload assembly, and retry: see references/app-x402-flow.md. On retry, target the URL from accepts[].resource if it was present in the challenge, otherwise the original request URL.
Domain warning. verifyingContract is the token contract, nota separate verifier. Usenameandversionfrom the challenge's
extra field, do not assume defaults. Different tokens have differentdomain values. An incorrect domain produces a signature the
facilitator will reject with another 402.
>
Bit-exact UTF-8 for `extra.name`. The EIP-712 domain hash is
byte-exact. Thenamefield inextramust be passed through
unchanged from the challenge bytes. Do not normalize it, do not
lowercase it, do not substitute ASCIIT(U+0054) for Unicode₮
(U+20AE), do not collapse Unicode forms (NFC vs NFD). For example, a
challenge that returns"USD₮0"must be signed as"USD₮0"; reading
it as "USDT0" will produce a signature the facilitator rejects withanother 402. Pass the raw bytes of extra.name straight into theEIP-712 domain.
Error Handling
| Situation | Action |
|---|---|
402 challenge has no network field | Inspect challenge body; if chainId resolves to 196 use this skill, otherwise escalate to pay-with-any-token |
| Network is not chain 196 | Escalate to pay-with-any-token |
x402Version !== 1 | Refuse cleanly; surface a version mismatch error. v1.0.0 of this skill targets x402 v1 only. |
accepts[].scheme !== "exact" | Refuse cleanly; v1.0.0 supports the exact scheme only. Other x402 schemes (upto, batch-settlement) are out of scope. |
accepts[].maxAmountRequired === "0" | Refuse cleanly as merchant misconfiguration. HTTP 402 demands a positive payment; OKX's facilitator will not settle a zero-value transfer. Do not sign and do not rationalize zero as a ping, auth check, or free tier. |
| APP requests USDC on X Layer | Surface a clear caveat: USDC has no reliable Uniswap v3 routing on X Layer. Suggest bridging USDC directly from a chain where it is liquid (Base, Arbitrum, Mainnet), or asking about USDT0. |
| Insufficient asset on X Layer | Trigger funding flow (Phase 3) |
| Trading API returns 400 | Log request/response; check amount formatting and address checksums |
| Trading API returns 429 | Back off and retry with exponential delay |
| Quote expired | Re-fetch quote; do not reuse old permitData |
| Bridge times out | Check Across bridge explorer; do not re-submit |
| EIP-3009 signature rejected (402 on retry) | Verify domain name / version from extra (byte-exact, including any non-ASCII characters), check validBefore is fresh, confirm nonce was unused |
| Amount mismatch on retry | Recompute base units using on-chain decimals() of the actual asset; do not assume 6 |
On-chain settlement reverts (transferFrom failed) | Re-check balanceOf at retry time; the balance may have been drained between sign and submit (shared wallet, concurrent agent, manual transfer). Surface to the user before retrying. |
User asks about escrow / session / batch / upto | Inform that this skill version covers Instant Payment (exact scheme) only. Other primitives are out of scope for v1.0.0; a v1.x follow-up will track them as OKX ships. |
Key Addresses and References
X Layer (chain 196)
- Chain ID:
196 - Public RPC:
https://rpc.xlayer.tech - USDT0:
0x779Ded0c9e1022225f8E0630b35a9b54bE713736(decimals 6) - USDG:
0x4ae46a509F6b1D9056937BA4500cb143933D2dc8(decimals 6) - USDC:
0x74b7F16337b8972027F6196A17a631aC6dE26d22(decimals 6, no
reliable Uniswap routing on X Layer; bridge from a liquid chain)
- WOKB (wrapped native):
0xe538905cf8410324e03A5A23C1c177a474D59b2b
(decimals 18)
- Uniswap V3 Factory:
0x4B2ab38DBF28D31D467aA8993f6c2585981D6804 - SwapRouter02:
0x4f0C28f5926AFDA16bf2506D5D9e57Ea190f9bcA - Universal Router 2.1:
0xDa00aE15d3A71466517129255255db7c0c0956d3
- QuoterV2:
0xD1b797D92d87B688193A2B976eFc8D577D204343 - Permit2:
0x000000000022D473030F116dDEE9F6B43aC78BA3 - USDT0/USDG pool:
0x0cBe0dBE1400e57f371a38BD3b9bC80F7C3676dA - USDT0/WOKB pool:
0x63d62734847E55A266FCa4219A9aD0a02D5F6e02
Uniswap Trading API
- Base URL:
https://trade-api.gateway.uniswap.org/v1 - Header:
x-api-key: $UNISWAP_API_KEY - Header:
x-universal-router-version: 2.1.1 - Supported chains include 1, 8453, 42161, 10, 137, 130, 196, and more
(see Trading API supported-chains docs).
The on-chain Universal Router contract on X Layer is labeled 2.1
(deployed at 0xDa00aE15d3A71466517129255255db7c0c0956d3 above). TheTrading API expects the header value 2.1.1, which is the API'sinternal version-string for the routing path that targets the same
Universal Router 2.1 contract. The two version strings refer to
related but distinct things; do not substitute one for the other.
OKX APP
- APP overview / dev docs:
https://web3.okx.com/onchainos/dev-docs/payments/x402-introduction
- OKX onchainos-skills repo:
https://github.com/okx/onchainos-skills - x402 spec:
https://github.com/coinbase/x402
Related Skills
- pay-with-any-token, sibling skill
for HTTP 402 challenges on chains other than X Layer (Ethereum, Base, Arbitrum, Tempo, etc.). Use that skill for non-X-Layer challenges.
- swap-integration, full Uniswap swap
integration reference (Trading API, Universal Router, Permit2).
APP x402 Flow on X Layer
OKX's APP Pay Per Use uses the x402 "exact" scheme on EVM. The payer signs an EIP-3009 TransferWithAuthorization off-chain. OKX's facilitator verifies and settles the transfer on chain 196 (zero gas to the payer).
Table of Contents
- Step 0: Prerequisites
- Step 1: Helpers and Validation
- Step 2: Confirm Pre-Signing State
- Step 3: Generate Nonce and Deadline
- Step 3.5: User Confirmation Gate
- Step 4: Sign the EIP-3009 Authorization
- Step 5: Construct the X-PAYMENT Payload
- Step 6: Retry the Original Request
- Step 7: Interpret the Response
Step 0: Prerequisites
Before running any block in this document, assert required CLI tools are installed. bc is needed for human-readable amount formatting and is not preinstalled on every macOS:
set -euo pipefail
command -v cast >/dev/null || { echo "cast required (foundry)" >&2; exit 1; }
command -v jq >/dev/null || { echo "jq required" >&2; exit 1; }
command -v bc >/dev/null || { echo "bc required (brew install bc)" >&2; exit 1; }
command -v openssl >/dev/null || { echo "openssl required" >&2; exit 1; }
command -v curl >/dev/null || { echo "curl required" >&2; exit 1; }
command -v node >/dev/null || { echo "node 18+ required (used by viem)" >&2; exit 1; }
command -v npm >/dev/null || { echo "npm required (used to install viem)" >&2; exit 1; }The X Layer RPC URL is overridable for rate-limiting or failover:
RPC_URL="${X_LAYER_RPC_URL:-https://rpc.xlayer.tech}"Resolve a viem-capable Node environment
Step 4 signs the EIP-3009 authorization with viem. Pick the directory the signer script will run from, in this order:
1. The user's current working directory, if viem/accounts is already resolvable there (zero install). 2. A cached scratch directory at ~/.cache/uniswap-pay-with-app/signer/ (or whatever X402_SIGNER_DIR is set to). Persists across runs. 3. If neither has viem, prompt the user via `AskUserQuestion` before installing. The user must know what is being installed on their machine. The summary you present must include: "package=viem, target=$X402_SIGNER_DIR, command=npm install viem, footprint=~13 packages and ~5 MB". Only run the install on an explicit yes. If the user declines, exit cleanly before any signing happens.
set -euo pipefail
X402_SIGNER_DIR="${X402_SIGNER_DIR:-$HOME/.cache/uniswap-pay-with-app/signer}"
viem_resolves_in() {
( cd "$1" && node -e "require.resolve('viem/accounts')" ) >/dev/null 2>&1
}
if viem_resolves_in .; then
SIGNER_CWD=.
elif viem_resolves_in "$X402_SIGNER_DIR"; then
SIGNER_CWD="$X402_SIGNER_DIR"
else
# Agent: invoke AskUserQuestion FIRST. Do not run the install lines below
# without an explicit user 'yes'. After confirmation:
mkdir -p "$X402_SIGNER_DIR"
( cd "$X402_SIGNER_DIR" && [ -f package.json ] || npm init -y >/dev/null )
( cd "$X402_SIGNER_DIR" && npm install viem --no-audit --no-fund --loglevel=error )
SIGNER_CWD="$X402_SIGNER_DIR"
fi
echo "viem resolved in: $SIGNER_CWD"The signer script in Step 4 must be invoked with cd "$SIGNER_CWD" so Node's module resolution finds viem there.
Step 1: Helpers and Validation
set -euo pipefail
get_token_decimals() {
local token_addr="$1" rpc_url="${2:-${X_LAYER_RPC_URL:-https://rpc.xlayer.tech}}"
local out
out=$(cast call "$token_addr" "decimals()(uint8)" --rpc-url "$rpc_url") || {
echo "ERROR: decimals() call failed for $token_addr on $rpc_url" >&2
return 1
}
[[ "$out" =~ ^[0-9]+$ ]] || {
echo "ERROR: non-numeric decimals returned: $out" >&2
return 1
}
echo "$out"
}
format_token_amount() {
local amount="$1" decimals="$2"
local result
result=$(echo "scale=$decimals; $amount / (10 ^ $decimals)" | bc -l | sed 's/0*$//' | sed 's/\.$//')
[ -z "$result" ] && result="0"
echo "$result"
}Always show the user human-readable amounts (e.g. 0.005 USDT0),not raw base units. get_token_decimals fails loudly on RPC error.Never default to 6: a wrong decimals value silently misleads theuser-facing confirmation gate. Every call site MUST check the exit
code: X402_DECIMALS=$(get_token_decimals "$X402_ASSET" "$RPC_URL") || exit 1.Validate every value pulled from the 402 body before using it in shell commands or signing payloads. The block in Step 2 below enforces these as hard gates, not advisory notes:
- Addresses match
^0x[a-fA-F0-9]{40}$ - Amounts match
^[0-9]+$ - URLs start with
https:// - Nonce matches
^0x[a-fA-F0-9]{64}$ - Reject any value containing
;,|,&,$, backtick, parentheses,
redirection, backslash, quotes, newlines
Step 2: Confirm Pre-Signing State
set -euo pipefail
# Required environment
: "${X402_SCHEME:?missing}" # must be "exact"
: "${X402_NETWORK:?missing}" # x-layer / xlayer / eip155:196 / 196
: "${X402_ASSET:?missing}" # token contract on X Layer
: "${X402_AMOUNT:?missing}" # base units, integer string
: "${X402_PAY_TO:?missing}" # recipient
: "${X402_RESOURCE:?missing}" # original URL (or accepts[].resource override)
: "${WALLET_ADDRESS:?missing}" # source wallet
# 0) Hard input-validation gates (no advisory notes; enforced)
[[ "$X402_ASSET" =~ ^0x[a-fA-F0-9]{40}$ ]] || { echo "bad asset address" >&2; exit 1; }
[[ "$X402_PAY_TO" =~ ^0x[a-fA-F0-9]{40}$ ]] || { echo "bad payTo address" >&2; exit 1; }
[[ "$WALLET_ADDRESS" =~ ^0x[a-fA-F0-9]{40}$ ]] || { echo "bad wallet address" >&2; exit 1; }
[[ "$X402_AMOUNT" =~ ^[0-9]+$ ]] || { echo "bad amount" >&2; exit 1; }
[[ "$X402_RESOURCE" =~ ^https://[A-Za-z0-9._~:/?\#@%=+,\&\;-]+$ ]] || { echo "bad resource URL" >&2; exit 1; }
# Defense in depth: the regex bracket class above already constrains the
# character set. This case block is a hard backstop against the
# highest-impact characters that could escape a quote, in case a future
# edit relaxes the regex. Note: `&` and `;` are valid sub-delims in real
# query strings (?a=1&b=2) and are admitted by the regex; they are NOT
# rejected here because $X402_RESOURCE is only ever interpolated inside
# double-quoted shell contexts (e.g. curl "$X402_RESOURCE"), where they
# cannot trigger word-splitting or command separation.
case "$X402_RESOURCE" in
*\`*|*\\*|*\"*|*\'*|*\|*|*\$*|*\(*|*\)*|*\<*|*\>*|*\**|*\!*|*$'\n'*)
echo "bad resource URL: contains shell metacharacter" >&2; exit 1 ;;
esac
# Note: $X402_RESOURCE should be set from accepts[selected].resource if the 402 challenge
# provides one (it can override the original request URL for proxied or redirected
# resources); fall back to the originally requested URL only if accepts[].resource is absent.
# If accepts[selected].resource's host differs from the host of the originally-requested
# URL, surface the mismatch to the user and require explicit confirmation before
# continuing. A facilitator-mediated redirect is legitimate but should not be silent.
if [ -n "${ORIGINAL_REQUEST_URL:-}" ]; then
# Strip userinfo (user:pass@) and port (:NNNN) before comparing so that
# equivalent hosts don't trip the gate.
strip_host() {
echo "$1" | awk -F/ '{print $3}' | sed -E 's/^[^@]+@//' | sed -E 's/:[0-9]+$//'
}
RESOURCE_HOST=$(strip_host "$X402_RESOURCE")
ORIGINAL_HOST=$(strip_host "$ORIGINAL_REQUEST_URL")
[ -n "$RESOURCE_HOST" ] && [ -n "$ORIGINAL_HOST" ] || {
echo "ERROR: could not parse host from URLs (resource=$X402_RESOURCE, original=$ORIGINAL_REQUEST_URL)" >&2
exit 1
}
if [ "$RESOURCE_HOST" != "$ORIGINAL_HOST" ]; then
if [ "${X402_HOST_MISMATCH_ACK:-}" != "yes" ]; then
echo "ERROR: accepts[].resource host ($RESOURCE_HOST) differs from original request host ($ORIGINAL_HOST)." >&2
echo "Surface this via AskUserQuestion. After explicit user confirmation, re-invoke with X402_HOST_MISMATCH_ACK=yes." >&2
exit 1
fi
fi
fi
# 1) Only "exact" scheme is supported in v1.0.0
[ "$X402_SCHEME" = "exact" ] || { echo "Unsupported scheme: $X402_SCHEME" >&2; exit 1; }
# 2) Network must resolve to chain 196
case "$X402_NETWORK" in
x-layer|xlayer|"eip155:196"|196) X402_CHAIN_ID=196 ;;
*) echo "Network is not X Layer. Use pay-with-any-token instead." >&2; exit 1 ;;
esac
# 3) Wallet must hold enough of the requested asset on X Layer
RPC_URL="${X_LAYER_RPC_URL:-https://rpc.xlayer.tech}"
ASSET_BALANCE=$(cast call "$X402_ASSET" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" --rpc-url "$RPC_URL") || {
echo "ERROR: balanceOf call failed; check RPC connectivity" >&2; exit 1;
}
[[ "$ASSET_BALANCE" =~ ^[0-9]+$ ]] || {
echo "ERROR: balanceOf returned non-integer: $ASSET_BALANCE" >&2; exit 1;
}
if [ "$ASSET_BALANCE" -lt "$X402_AMOUNT" ]; then
X402_DECIMALS=$(get_token_decimals "$X402_ASSET" "$RPC_URL") || exit 1
HAVE=$(format_token_amount "$ASSET_BALANCE" "$X402_DECIMALS")
NEED=$(format_token_amount "$X402_AMOUNT" "$X402_DECIMALS")
echo "Insufficient asset balance on X Layer. Have $HAVE, need $NEED."
echo "Run the funding flow (references/funding-x-layer.md), then return here."
exit 1
fi
# When the funding flow runs, it captures SOURCE_TX_HASH from the Trading
# API /swap response. See funding-x-layer.md for the capture + validate
# pattern (jq with `// empty` fallback plus a 0x[a-fA-F0-9]{64} shape
# check); a literal "null" or empty string must fail the funding flow
# rather than propagating into this script.
# Capture 402 challenge freshness for the sign-time gate (see Step 4).
# Named CHALLENGE_FETCHED_AT to disambiguate from the Trading API "quote"
# concept used in funding-x-layer.md; here it refers to the 402 body itself.
CHALLENGE_FETCHED_AT=$(date +%s)Step 3: Generate Nonce and Deadline
set -euo pipefail
X402_NONCE="0x$(openssl rand -hex 32)" # 32-byte random nonce
# Assert the nonce is the right shape. If openssl is missing or fails,
# command substitution can yield "0x" (empty), which signs against
# nonce: 0. First time works, second is a replay. Fail loud here.
[ ${#X402_NONCE} -eq 66 ] || { echo "openssl missing or failed: nonce length=${#X402_NONCE}" >&2; exit 1; }
[[ "$X402_NONCE" =~ ^0x[a-fA-F0-9]{64}$ ]] || { echo "bad nonce shape: $X402_NONCE" >&2; exit 1; }
X402_VALID_AFTER=0 # immediately valid
# Default the requested timeout to 5 minutes if not provided by the challenge.
X402_TIMEOUT="${X402_TIMEOUT:-300}"
# `maxTimeoutSeconds` from the challenge body is an upper bound on
# (validBefore - validAfter). Default the ceiling to the requested
# timeout if the challenge omits it, then clamp.
X402_MAX_TIMEOUT="${X402_MAX_TIMEOUT:-$X402_TIMEOUT}"
if [ "$X402_TIMEOUT" -lt "$X402_MAX_TIMEOUT" ]; then
X402_EFFECTIVE_TIMEOUT="$X402_TIMEOUT"
else
X402_EFFECTIVE_TIMEOUT="$X402_MAX_TIMEOUT"
fi
X402_VALID_BEFORE=$(( $(date +%s) + X402_EFFECTIVE_TIMEOUT ))The challenge body's maxTimeoutSeconds is an upper bound on validBefore - validAfter. The clamp above keeps the request inside the bound while leaving the facilitator time to settle.
Step 3.5: User Confirmation Gate
This step is mandatory and not optional. Do not auto-submit. Use AskUserQuestion (or the equivalent confirmation primitive in the host agent) to surface a payment summary and obtain explicit yes/no consent before signing:
- Token:
$X402_TOKEN_NAME($X402_ASSET) on X Layer (chain 196) - Amount: human-readable amount + base units
- Recipient:
$X402_PAY_TO - Resource:
$X402_RESOURCE - Expiry:
validBefore(UTC + epoch) - Nonce (first 10 chars of
$X402_NONCE, for traceability)
If the user declines, abort. If the user does not respond, abort. Do not proceed to Step 4 without an affirmative answer in the transcript.
Step 4: Sign the EIP-3009 Authorization
The EIP-712 domain uses the token contract's own name and version (taken verbatim from the challenge's extra field). verifyingContract is the token contract itself.
Unicode warning. USDT0's domainnameis"USD₮0"with the
Unicode trademark sign₮(U+20AE), not an ASCIIT. EIP-712 hashes
the domain name as byte-exact UTF-8: pass it through unchanged fromextra.name. Do not normalize. Do not substitute ASCIIT. Any
mutation produces a signature the facilitator will reject.
Freshness gate (refuse-to-sign-when-stale): a signed-but-stale authorization burns a nonce on the facilitator side; refusing to sign preserves the nonce. Run this gate before invoking signTypedData, not after. The challenge body's prices and accepts[] parameters are short-lived (the SKILL doc states roughly 60 seconds); 45 is a safe ceiling that leaves margin:
set -euo pipefail
if [ $(($(date +%s) - CHALLENGE_FETCHED_AT)) -ge 45 ]; then
echo "402 challenge is older than 45 seconds; refetch before signing." >&2
exit 1
fiSign with viem:
import { privateKeyToAccount } from 'viem/accounts';
// Validate every input before constructing the typed-data payload.
// `process.env.FOO!` casts hide undefined and empty-string bugs; an
// empty domain or message field produces a valid-looking signature
// the facilitator will reject, burning a fresh nonce.
function requireEnv(key: string): string {
const v = process.env[key];
if (!v || !v.trim()) {
throw new Error(
`${key} is unset or empty. Re-parse the corresponding field from the 402 challenge.`
);
}
return v;
}
// Shape-validating wrappers: catch the case where a string is non-empty but
// the wrong shape (e.g. truncated address, scientific-notation amount). An
// empty-only check still produces a valid-looking signature the facilitator
// will reject, burning a fresh nonce.
function requireAddress(key: string): `0x${string}` {
const v = requireEnv(key);
if (!/^0x[a-fA-F0-9]{40}$/.test(v)) {
throw new Error(`${key} is not a valid 0x address: ${v}`);
}
return v as `0x${string}`;
}
function requireUint(key: string): bigint {
const v = requireEnv(key);
if (!/^[0-9]+$/.test(v)) {
throw new Error(`${key} is not a non-negative integer: ${v}`);
}
return BigInt(v);
}
function requireBytes32(key: string): `0x${string}` {
const v = requireEnv(key);
if (!/^0x[a-fA-F0-9]{64}$/.test(v)) {
throw new Error(`${key} is not a 0x bytes32: ${v}`);
}
return v as `0x${string}`;
}
// Free-text fields (extra.name, extra.version) feed the EIP-712 domain
// bit-exact and may also be surfaced to the user. Per the SKILL.md
// Input Validation Rules, reject the whole challenge rather than
// mutating the value if it contains shell metacharacters.
function requireSafeText(key: string): string {
const v = requireEnv(key);
if (/[;|&$`()<>\\'"\n]/.test(v)) {
throw new Error(
`${key} contains shell metacharacters; reject the whole challenge per skill policy.`
);
}
return v;
}
const privateKey = requireEnv('PRIVATE_KEY');
const tokenName = requireSafeText('X402_TOKEN_NAME'); // from extra.name (e.g. "USD₮0")
const tokenVersion = requireSafeText('X402_TOKEN_VERSION'); // from extra.version (e.g. "1")
const walletAddress = requireAddress('WALLET_ADDRESS');
const x402Asset = requireAddress('X402_ASSET');
const x402PayTo = requireAddress('X402_PAY_TO');
const x402Amount = requireUint('X402_AMOUNT');
const x402ValidAfter = requireUint('X402_VALID_AFTER');
const x402ValidBefore = requireUint('X402_VALID_BEFORE');
const x402Nonce = requireBytes32('X402_NONCE');
const account = privateKeyToAccount(privateKey as `0x${string}`);
const domain = {
name: tokenName,
version: tokenVersion,
chainId: 196,
verifyingContract: x402Asset,
};
// REQUIRED: AskUserQuestion confirmation already happened in Step 3.5.
// Do not reach this line without an affirmative user answer.
const signature = await account.signTypedData({
domain,
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message: {
from: walletAddress,
to: x402PayTo,
value: x402Amount,
validAfter: x402ValidAfter,
validBefore: x402ValidBefore,
nonce: x402Nonce,
},
});
process.env.X402_SIGNATURE = signature;To execute the snippet above, write it to a .mjs file and run Node from the directory Step 0 resolved viem in ($SIGNER_CWD). Capture stdout into X402_SIGNATURE, then verify the shape before continuing, because set -euo pipefail does not fire on a failed command substitution unless inherit_errexit is set (which is bash 4.4+ only, not available on default macOS bash 3.2):
set -euo pipefail
SIGNER_SCRIPT=$(mktemp -t x402-signer-XXXXXX).mjs
trap 'rm -f "$SIGNER_SCRIPT"' EXIT
cat > "$SIGNER_SCRIPT" <<'JS'
// Paste the signing snippet above here, with `process.stdout.write(signature)`
// at the end instead of assigning to process.env.
JS
SIG_FILE=$(mktemp)
( cd "$SIGNER_CWD" && node "$SIGNER_SCRIPT" > "$SIG_FILE" )
X402_SIGNATURE=$(cat "$SIG_FILE")
rm -f "$SIG_FILE"
if ! [[ "$X402_SIGNATURE" =~ ^0x[0-9a-fA-F]{130}$ ]]; then
echo "ERROR: signer returned bad signature shape" >&2
exit 1
fi
export X402_SIGNATUREDomain warning. verifyingContract is the token contract(X402_ASSET), not a separate verifier. Usenameandversion
from extra. Do not assume defaults. An incorrect domain produces asignature the facilitator will reject with another 402.
Step 5: Construct the X-PAYMENT Payload
The wire shape MUST match the x402 v1 spec §5.2 PaymentPayload schema exactly:
{ x402Version, scheme, network, payload: { signature, authorization } }Per spec §5.2.1, value, validAfter, and validBefore are all string-typed (uint256-as-decimal-string for value, Unix-timestamp-as-decimal-string for the timestamps). Use --arg, not --argjson, so jq emits JSON strings rather than numbers.
x402Version is an integer per spec (not a string-typed field like the EIP-3009 timestamps): --argjson x402Version 1 emits a JSON number, which is correct. If a future spec revision retypes this field as a string, switch to --arg instead.
set -euo pipefail
X402_PAYMENT_JSON=$(jq -n \
--argjson x402Version 1 \
--arg scheme "$X402_SCHEME" \
--arg network "$X402_NETWORK" \
--arg from "$WALLET_ADDRESS" \
--arg to "$X402_PAY_TO" \
--arg value "$X402_AMOUNT" \
--arg validAfter "$X402_VALID_AFTER" \
--arg validBefore "$X402_VALID_BEFORE" \
--arg nonce "$X402_NONCE" \
--arg sig "$X402_SIGNATURE" \
'{
x402Version: $x402Version,
scheme: $scheme,
network: $network,
payload: {
signature: $sig,
authorization: {
from: $from,
to: $to,
value: $value,
validAfter: $validAfter,
validBefore: $validBefore,
nonce: $nonce
}
}
}')
# Base64-encode and strip whitespace (header spec requires no newlines)
X402_PAYMENT=$(echo "$X402_PAYMENT_JSON" | base64 | tr -d '[:space:]')value, validAfter, and validBefore MUST be strings (--arg, not --argjson): uint256 amounts exceed JSON's safe integer range, and the spec's PaymentPayload schema types all three as string. Top-level chainId and asset are intentionally omitted: network already encodes the chain, and the asset is implicit in the requirement matching.
Step 6: Retry the Original Request
The freshness gate has already run in Step 4 (refuse-to-sign-when-stale). By the time control reaches this step, the signature is fresh and the nonce is committed.
set -euo pipefail
# Capture status and headers explicitly. `head -1 | grep -o '[0-9]\{3\}'`
# is fragile (HTTP/2, 100-continue interim responses), so use curl's
# own status-code writer.
RETRY_HEADERS=$(mktemp)
RETRY_BODY_FILE=$(mktemp)
trap 'rm -f "$RETRY_HEADERS" "$RETRY_BODY_FILE"' EXIT
RETRY_STATUS=$(curl -s -o "$RETRY_BODY_FILE" -D "$RETRY_HEADERS" \
-w '%{http_code}' \
"$X402_RESOURCE" \
-H "X-PAYMENT: $X402_PAYMENT" \
-H "Content-Type: application/json")
RETRY_BODY=$(cat "$RETRY_BODY_FILE")
# Use awk for header parsing rather than `cut -d' ' -f2-`: header names
# may have variable whitespace after the colon, and `cut` mishandles
# tabs and folded continuations.
X402_PAYMENT_RESPONSE=$(awk 'BEGIN{IGNORECASE=1} /^x-payment-response:/ { sub(/^[^:]+:[ \t]*/, ""); print }' \
"$RETRY_HEADERS" | tr -d '\r\n')
echo "HTTP status: $RETRY_STATUS"Retry policy (two attempts maximum)
The 402 path is bounded: at most one retry after the initial 402, and only against a freshly re-parsed challenge. The retry budget is tracked at the agent level, not via a bash counter. Bash variable state does not survive across separate Bash tool invocations, so a counter would silently reset and permit unbounded retries.
Agent-level retry instruction: If the retry returns 402, you may attempt one more retry, but only after re-deriving X402_NONCE and X402_VALID_BEFORE from a freshly fetched 402 challenge. Do not retry a third time. After the second 402, surface the rejection to the user with the exact body OKX returned and stop.
To enforce the cap across separate Bash tool invocations (where shell variable state does not persist), use a tmpfile-based stamp and pass X402_ATTEMPT_FILE through to subsequent invocations so the budget is shared:
# IMPORTANT: $$ would evaluate to a fresh PID on every Bash tool invocation,
# defeating the cap. The agent MUST export X402_ATTEMPT_FILE explicitly before
# the first invocation of this block (e.g. /tmp/x402-attempt-${WALLET_ADDRESS}-${X402_NONCE_PREFIX})
# and reuse the SAME path across retries.
: "${X402_ATTEMPT_FILE:?missing: agent must set a stable path so the retry budget persists across Bash invocations}"
[ -e "$X402_ATTEMPT_FILE" ] && [ ! -r "$X402_ATTEMPT_FILE" ] && {
echo "ERROR: X402_ATTEMPT_FILE exists but is unreadable: $X402_ATTEMPT_FILE" >&2
exit 1
}
attempts=$(wc -l < "$X402_ATTEMPT_FILE" 2>/dev/null || echo 0)
[ "$attempts" -lt 2 ] || { echo "Retry budget exhausted (max 2 attempts)." >&2; exit 1; }
date +%s >> "$X402_ATTEMPT_FILE"Variable lifecycle on retry or re-confirmation. On any retry, you MUST regenerate the following from a freshly fetched 402 challenge body before re-signing:
X402_NONCE(Step 3): never reuse a prior nonce; the facilitator
rejects replays and you would burn the new attempt for nothing.
X402_VALID_BEFORE(Step 3): recompute from the freshdate +%sso
the freshness gate in Step 4 does not trip on an old deadline.
CHALLENGE_FETCHED_AT(Step 2): re-capture fromdate +%sat the
moment the new 402 body is read; this is the timestamp the Step 4 gate compares against.
The user-confirmation gate (Step 3.5) MUST also re-prompt; do not silently re-sign on prior consent.
Step 7: Interpret the Response
| Status | Meaning | Action |
|---|---|---|
| 200 | Payment accepted, resource delivered | If X402_PAYMENT_RESPONSE is non-empty, decode it: `echo "$X402_PAYMENT_RESPONSE" \ |
| 402 | Payment rejected | Most common causes: wrong domain name / version, expired validBefore, reused nonce, amount mismatch. Re-derive from the fresh challenge and try once more (max two attempts total) |
| 400 | Malformed payload | Verify JSON structure and base64 encoding (no whitespace), confirm x402Version: 1 and payload.{signature, authorization} shape |
| 5xx | Facilitator or origin error | Surface raw body to the user. Do not auto-retry |
Do not retry indefinitely on 402. Two attempts maximum, then surface the rejection details to the user with the exact message OKX returned. Empty X402_PAYMENT_RESPONSE is not an error: skip the decode step and report success without a receipt rather than feeding empty input to base64 --decode.
Funding USDT0 on X Layer via the Uniswap Trading API
When the wallet lacks the asset required by an APP Pay Per Use 402 challenge, acquire it on X Layer (chain 196) using the Uniswap Trading API. The API supports both same-chain swaps on X Layer and cross-chain routing into X Layer (powered by Across).
Table of Contents
- Decide the funding target
- Pick the source chain and token
- Phase A: Same-chain swap on X Layer
- Phase B: Cross-chain bridge into X Layer
- Verify the destination balance
Decide the Funding Target
| Asset on X Layer | Recommended? | Notes |
|---|---|---|
USDT0 (0x779Ded0c…) | ✅ default | Deepest Uniswap v3 liquidity (USDT0/USDG and USDT0/WOKB pools; additional pools at other fee tiers may be deployed, and the Trading API will pick the optimal route across all available pools). Use this if the 402 challenge accepts USDT0. |
USDG (0x4ae46a50…) | ✅ supported | Reachable via direct Trading API quote, or one-hop USDT0 to USDG. |
USDC (0x74b7F163…) | ❌ not via Uniswap on X Layer | Trading API does not consistently return routes for USDC swaps on X Layer despite pools at 0.05% and 0.3% existing: TVL is too thin for reliable execution. If the merchant requires USDC, bridge USDC directly from a chain where it is liquid (Base, Arbitrum, Mainnet) using the Trading API rather than attempting a same-chain swap on X Layer. |
Default funding target = USDT0. Override only when the 402
challenge demands a different specific asset and that asset is funded
by an entry above with ✅.
Pick the Source Chain and Token
Inspect the user's ERC-20 holdings across supported source chains and prefer cheapest gas + deepest liquidity to the destination.
set -euo pipefail
# USDC on Base (cheapest bridge gas)
cast call 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url https://mainnet.base.org
# USDC on Ethereum
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url https://eth.llamarpc.com
# Native ETH on Base / Ethereum (use zero address for the swap input)
cast balance "$WALLET_ADDRESS" --rpc-url https://mainnet.base.orgPath priority for landing USDT0 on X Layer:
1. Source already holds USDT0 on X Layer, skip funding entirely. 2. Source holds USDG on X Layer, same-chain swap USDG to USDT0 (Phase A, single hop). 3. Source holds USDT0 on a different chain, Phase B cross-chain (likely cheapest). 4. Wallet has a stablecoin (USDC) on Base / Arbitrum / Mainnet, cross-chain route to USDT0 on X Layer (Phase B). 5. Wallet has native ETH on Base / Mainnet, cross-chain route from native (zero address 0x0000000000000000000000000000000000000000) to USDT0 on X Layer (Phase B handles swap + bridge in one quote). 6. Wallet only holds non-USDT0 tokens on X Layer, same-chain swap (Phase A).
Phase A: Same-Chain Swap on X Layer
Use this when the wallet already holds a token on X Layer (e.g. WOKB or USDG) and needs to convert it to the asset required by the 402 challenge. Skip if the wallet has no relevant tokens on X Layer.
Confirmation gate before approval and before broadcast.
>
Pre-flight: OKB balance check. Same-chain X Layer swap requires
OKB for gas. Confirm the wallet has OKB before proceeding:
>
```bash
set -euo pipefail
OKB_BAL=$(cast balance "$WALLET_ADDRESS" \
--rpc-url "${X_LAYER_RPC_URL:-https://rpc.xlayer.tech}")
# Non-zero OKB sanity check. This does not guarantee enough gas to broadcast
# a swap; it only catches the common "wallet has literally zero OKB" case.
# Replace with a real threshold (e.g. 0.001 OKB) if you want to gate on
# usable gas.
[ "$OKB_BAL" != "0" ] || {
echo "Same-chain X Layer swap requires OKB for gas. Wallet has 0 OKB. Either acquire OKB first, or route entirely cross-chain (Phase B) which only needs source-chain gas." >&2
exit 1
}
```
set -euo pipefail
QUOTE_FETCHED_AT=$(date +%s)
QUOTE=$(curl -fsS -X POST https://trade-api.gateway.uniswap.org/v1/quote \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
-H "x-universal-router-version: 2.1.1" \
-d "$(jq -n \
--arg type "EXACT_OUTPUT" \
--argjson tokenInChainId 196 \
--argjson tokenOutChainId 196 \
--arg tokenIn "$SOURCE_TOKEN_XLAYER" \
--arg tokenOut "$X402_ASSET" \
--arg amount "$X402_AMOUNT" \
--arg swapper "$WALLET_ADDRESS" \
'{
type: $type,
tokenInChainId: $tokenInChainId,
tokenOutChainId: $tokenOutChainId,
tokenIn: $tokenIn,
tokenOut: $tokenOut,
amount: $amount,
swapper: $swapper,
urgency: "normal"
}')") || { echo "Trading API quote failed" >&2; exit 1; }Then check_approval (only if tokenIn is not native), build the permit signature when required, and broadcast via /swap. Detailed check_approval + permit + /swap flow is identical to the `pay-with-any-token` flow. See that reference and substitute the X Layer chain ID and addresses.
Before broadcasting via /swap, gate on quote freshness:
set -euo pipefail
ELAPSED=$(($(date +%s) - QUOTE_FETCHED_AT))
[ "$ELAPSED" -lt 45 ] || {
echo "Quote is $ELAPSED seconds old; refetch before broadcasting." >&2
exit 1
}Phase B: Cross-Chain Bridge into X Layer
Important: Across coverage gap (verified 2026-04-27). The Uniswap
Trading API uses Across Protocol for cross-chain routing. **Across
does not currently include X Layer (chain 196) in its destination
list.** Live/quotecalls withtokenOutChainId=196and a different
tokenInChainIdreturnResourceNotFound: No quotes availablefor
every source chain we tested (Ethereum, Base, Arbitrum, native ETH).
Same-chain X Layer swaps (Phase A) work normally.
>
What this means for the agent. Until Across adds X Layer, the
Trading API cannot bridge tokens onto X Layer in a single quote. If
the user holds funds on a chain other than X Layer, the cross-chain
leg must be done through a bridge service that supports X Layer as a
destination (the user must run that step outside this skill, then
re-invoke for the same-chain swap + 402 settlement).
>
TODO (research before mid-May launch follow-up): confirm and
document the recommended X-Layer-supporting bridge for Uniswap+OKX
co-marketing alignment. Until then, do not prescribe a specific bridge
service in user-facing responses; surface the limitation honestly and
let the user choose.
Source-chain shortfall preflight (REQUIRED before quoting)
Before calling /quote or recommending a bridge, verify the user's source-chain wallet actually has enough of `SOURCE_TOKEN` to cover the required output amount plus fees. The most common funding failure is not the route, it is the user's source balance: e.g. user has 5 USDC on Base but the 402 demands 100 USDT0 on X Layer. Recommending "bridge 100.5 USDC from Base" in that situation is wrong; the user does not have 100.5 USDC to bridge.
set -euo pipefail
# X402_AMOUNT is in base units of the X Layer DESTINATION asset (e.g.
# USDT0 6 decimals). For a same-decimal source token (USDC also 6), the
# minimum source amount needed is X402_AMOUNT plus buffer. For a
# different-decimal source token, scale appropriately and consult an
# oracle or quote for an estimate. The check below uses the same-decimal
# stablecoin path (USDC -> USDT0, USDG -> USDT0, etc.). For different
# decimals or non-stable source tokens, fetch a price quote first and
# use the input-amount it returns to gate this check.
SOURCE_REQUIRED_BASE_UNITS=$(python3 -c "print(($X402_AMOUNT * 1005) // 1000)")
SOURCE_BALANCE=$(cast call "$SOURCE_TOKEN_ADDRESS" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "$SOURCE_CHAIN_RPC_URL")
# Strip cast's "(uint256)" suffix if present and compare via python (uint256-safe).
SOURCE_BALANCE_RAW=$(echo "$SOURCE_BALANCE" | awk '{print $1}')
if ! python3 -c "import sys; sys.exit(0 if int('$SOURCE_BALANCE_RAW') >= int('$SOURCE_REQUIRED_BASE_UNITS') else 1)"; then
echo "ERROR: source-chain shortfall on $SOURCE_CHAIN_NAME." >&2
echo " needed (base units): $SOURCE_REQUIRED_BASE_UNITS" >&2
echo " have (base units): $SOURCE_BALANCE_RAW" >&2
echo "Cannot fund the 402 from this source. Ask the user for an" >&2
echo "alternative source chain, a smaller payment amount, or to" >&2
echo "top up the source wallet first." >&2
exit 1
fiRefusing here is the correct behavior: a bridge instruction the user cannot execute is worse than no instruction. If multiple source chains are available and one has the funds, suggest that one. If none do, surface the shortfall in plain language and stop, do not auto-pivot into a different funding plan without re-confirming with the user via AskUserQuestion.
Quote and bridge
The block below describes the design intent: a single Trading API quote that handles swap + bridge into X Layer. It is preserved so the skill works without code changes the day Across adds X Layer. Today, expect the quote call to return ResourceNotFound. When that happens, fall through to the user-handoff path documented at the end of this section.
Apply a 0.5% buffer to compute X402_AMOUNT_WITH_BUFFER from X402_AMOUNT to absorb bridge fees. If the shortfall is < $5 worth, top up to $5 to amortize source chain gas.
set -euo pipefail
# Apply 0.5% buffer (uint256-safe integer math via python)
X402_AMOUNT_WITH_BUFFER=$(python3 -c "print(($X402_AMOUNT * 1005) // 1000)")
[[ "$X402_AMOUNT_WITH_BUFFER" =~ ^[0-9]+$ ]] || { echo "buffer math failed" >&2; exit 1; }
QUOTE_FETCHED_AT=$(date +%s)
# Capture HTTP status and body separately so we can distinguish:
# (a) HTTP 200 success -> proceed with the original Phase B flow
# (b) errorCode=ResourceNotFound (Across coverage gap) -> deferred-bridge handoff
# (c) network errors / 5xx -> surface as transient, advise retry
#
# IMPORTANT: do NOT use `curl -f` here. Under `set -e` a non-zero curl
# exit would terminate the script before we read $? into QUOTE_HTTP_STATUS,
# making the deferred-bridge branch unreachable on the exact failure
# path it is designed to handle. We capture status with `-w` instead.
QUOTE_BODY_FILE=$(mktemp)
trap 'rm -f "$QUOTE_BODY_FILE"' EXIT
QUOTE_HTTP_STATUS=$(curl -sS -X POST https://trade-api.gateway.uniswap.org/v1/quote \
-o "$QUOTE_BODY_FILE" \
-w '%{http_code}' \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
-H "x-universal-router-version: 2.1.1" \
-d "$(jq -n \
--arg type "EXACT_OUTPUT" \
--argjson tokenInChainId "$SOURCE_CHAIN_ID" \
--argjson tokenOutChainId 196 \
--arg tokenIn "$SOURCE_TOKEN" \
--arg tokenOut "$X402_ASSET" \
--arg amount "$X402_AMOUNT_WITH_BUFFER" \
--arg swapper "$WALLET_ADDRESS" \
'{
type: $type,
tokenInChainId: $tokenInChainId,
tokenOutChainId: $tokenOutChainId,
tokenIn: $tokenIn,
tokenOut: $tokenOut,
amount: $amount,
swapper: $swapper,
urgency: "normal"
}')") || {
# curl itself failed (DNS, TLS, network unreachable, etc.) -> case (c)
echo "ERROR: Trading API call failed at the network layer (curl exit \$? = $?)." >&2
echo "This is most likely a transient connectivity issue (DNS, TLS, network)." >&2
echo "Advise the user to retry; do NOT route them to an external bridge for this case." >&2
exit 1
}
QUOTE=$(cat "$QUOTE_BODY_FILE")
QUOTE_ERROR_CODE=$(echo "$QUOTE" | jq -r '.errorCode // empty' 2>/dev/null || echo "")Branch on the result:
set -euo pipefail
if [ "$QUOTE_HTTP_STATUS" = "200" ] && [ -z "$QUOTE_ERROR_CODE" ]; then
: # success -> continue with permitData + /swap on the source chain
elif [ "$QUOTE_ERROR_CODE" = "ResourceNotFound" ]; then
: # case (b): the deferred-bridge handoff below
elif [ "$QUOTE_HTTP_STATUS" -ge 500 ] 2>/dev/null; then
echo "ERROR: Trading API returned HTTP $QUOTE_HTTP_STATUS (server error)." >&2
echo "This is most likely transient. Advise the user to retry shortly." >&2
exit 1
else
echo "ERROR: Trading API returned HTTP $QUOTE_HTTP_STATUS, errorCode=$QUOTE_ERROR_CODE." >&2
echo "Body: $QUOTE" >&2
echo "Surface raw body to the user; do not route to an external bridge automatically." >&2
exit 1
fiIf errorCode is ResourceNotFound, the Trading API cannot currently deliver to X Layer cross-chain. This is the expected state today (Across does not list X Layer as a destination). Surface a clear message to the user via AskUserQuestion:
"The Uniswap Trading API does not currently support X Layer as a
cross-chain destination via Across Protocol (verified
2026-04-27). To pay this APP merchant, please bridge USDT0 (or
another stablecoin that lands as USDT0 on X Layer) to your wallet
using a bridge service that supports X Layer destinations. Once
the funds arrive on X Layer, re-invoke this skill and we will
handle the same-chain swap (if needed) and the 402 settlement."
Do NOT recommend a specific bridge product to the user in this skill version; the v1.0.0 stance is "any bridge that supports X Layer is fine; pick what you trust." A future skill version may add a co-marketing-aligned bridge recommendation once verified.
If the quote call DOES succeed (i.e. Across has shipped X Layer support since this doc was written), continue with the original flow: quote response contains permitData (sign with EIP-712), the /swap endpoint returns the calldata to broadcast on the source chain, and Across handles the X Layer arrival.
Before broadcasting via /swap, gate on quote freshness:
set -euo pipefail
ELAPSED=$(($(date +%s) - QUOTE_FETCHED_AT))
[ "$ELAPSED" -lt 45 ] || {
echo "Quote is $ELAPSED seconds old; refetch before broadcasting." >&2
exit 1
}When you broadcast the source-chain transaction, capture the resulting hash into SOURCE_TX_HASH (used in the bridge timeout message below):
SOURCE_TX_HASH=$(echo "$SWAP_RESPONSE" | jq -r '.transactionHash // empty')
[[ "$SOURCE_TX_HASH" =~ ^0x[a-fA-F0-9]{64}$ ]] || {
echo "no tx hash from /swap response" >&2
exit 1
}Bridge recipient. The Trading API delivers funds to the same
swapper address on chain 196. If the user wants the funds at adifferent X Layer address (e.g. an OKX Agentic Wallet they custody
separately), an extra transfer transaction on X Layer is required
after the bridge confirms.
>
Quotes expire in ~60 seconds. Re-fetch if any delay before
broadcast (the freshness gate above enforces a 45s ceiling).
>
Retry hygiene. On any retry of the quote-then-broadcast cycle,
re-derive both QUOTE_FETCHED_AT (the freshness timestamp) andX402_AMOUNT_WITH_BUFFER (the buffered output amount) from the newquote. Reusing stale values from an earlier attempt will either trip
the freshness gate or quote against an outdated buffer.
Verify the Destination Balance
After the bridge or same-chain swap completes, poll for the asset arrival on X Layer before returning to the EIP-3009 signing step. The loop tolerates transient RPC failures and validates that the returned balance is a non-negative integer.
If after 10 minutes the wallet still has insufficient $X402_ASSET, the funds may have arrived at a different token address (rare for current Across paths to X Layer) or the bridge may have failed. Surface the ambiguity to the user with the source-chain tx hash and the Across explorer link, and ask them to verify on-chain. v1.0.0 does not auto-detect alternate-token arrival on X Layer.
set -euo pipefail
# Assert prerequisites are set. SOURCE_TX_HASH must have been captured
# from the /swap response before entering the polling loop.
: "${SOURCE_TX_HASH:?missing, capture from /swap response before polling}"
: "${X402_ASSET:?missing}"
: "${X402_AMOUNT:?missing}"
: "${WALLET_ADDRESS:?missing}"
# Track successful RPC reads so we can distinguish "20 RPC failures" from
# "20 successful reads, all under target" at the end.
RPC_SUCCESS_COUNT=0
for i in {1..20}; do
XLAYER_BAL=$(cast call "$X402_ASSET" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "${X_LAYER_RPC_URL:-https://rpc.xlayer.tech}") || {
echo "RPC failure on attempt $i, retrying..." >&2
sleep 5
continue
}
[[ "$XLAYER_BAL" =~ ^[0-9]+$ ]] || {
echo "Non-integer balance: $XLAYER_BAL" >&2
sleep 5
continue
}
RPC_SUCCESS_COUNT=$((RPC_SUCCESS_COUNT + 1))
if [ "$XLAYER_BAL" -ge "$X402_AMOUNT" ]; then
echo "Funded. Balance: $XLAYER_BAL"
break
fi
echo "Waiting for arrival... attempt $i/20 (balance: $XLAYER_BAL base units)"
sleep 30
done
# If every attempt was an RPC failure, surface that distinctly before the
# generic "no usable balance" check below.
[ "$RPC_SUCCESS_COUNT" -gt 0 ] || {
echo "ERROR: all 20 attempts were RPC failures; bridge state unknown." >&2
echo "Source tx: $SOURCE_TX_HASH. Check https://app.across.to/transactions before re-submitting." >&2
exit 1
}
# Assert we have a usable balance reading. No `:-0` defaults here, those
# would defeat `set -u` and silently coerce a missing read into "below
# target".
[[ -n "${XLAYER_BAL:-}" && "$XLAYER_BAL" =~ ^[0-9]+$ ]] || {
echo "ERROR: bridge polling completed without a successful RPC read." >&2
echo "20 RPC failures or non-integer responses; cannot determine arrival state." >&2
echo "Source tx: $SOURCE_TX_HASH. Check https://app.across.to/transactions before re-submitting." >&2
exit 1
}
[ "$XLAYER_BAL" -ge "$X402_AMOUNT" ] || {
echo "Bridge not confirmed after 10 minutes. Wallet still holds $XLAYER_BAL of $X402_ASSET on X Layer (need $X402_AMOUNT)." >&2
echo "Source tx: $SOURCE_TX_HASH." >&2
echo "The funds may have arrived at a different token address (rare for current Across paths to X Layer) or the bridge may have failed." >&2
echo "Verify on-chain via https://app.across.to/transactions and https://www.oklink.com/x-layer/address/$WALLET_ADDRESS before re-submitting." >&2
exit 1
}Once funded, return to `app-x402-flow.md` to sign the EIP-3009 authorization and retry the original request.
Retry target URL. When the funding flow ends and we return to
the EIP-3009 signing in app-x402-flow.md, the URL to retry isaccepts[].resource if present in the original 402 challenge,otherwise the original request URL.