AgentPay-SDK-Go is an early-stage Go project in the AI payments / x402 ecosystem. It currently has 0 GitHub stars and 0 forks.
Go client for the Agent Tech payment API — create intents, execute USDC / USDT0 / USDT transfers across chains, and query status.
Client that auto-selects the API prefix based on auth:WithBearerAuth) → /v2 prefix — create intent → execute (backend signs with Agent wallet)/api prefix (public mode) — create intent → payer signs X402 & pays → submit settle_proofpayer_chain and any supported target_chain (defaults to base when omitted)usdc (default), usdt0, or usdt; the payer and target legs are independent, so you can pay USDT0 on Arbitrum and settle USDC on Base, or settle USDT0 cross-chain (defaults to usdc when omitted)go get github.com/cross402/usdc-sdk-go
package main
import (
"context"
"log"
"github.com/cross402/usdc-sdk-go"
)
func main() {
client, err := pay.NewClient(
"https://api-pay.agent.tech",
pay.WithBearerAuth("your-api-key", "your-secret-key"),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// 1. Create intent (multichain: pay from Base, settle on Solana)
resp, err := client.CreateIntent(ctx, &pay.CreateIntentRequest{
Email: "merchant@example.com",
Amount: "100.50",
PayerChain: pay.ChainBase,
TargetChain: pay.ChainSolanaMainnet, // omit to default to "base"
})
if err != nil {
log.Fatal(err)
}
log.Printf("Intent ID: %s (settling on %s)", resp.IntentID, resp.TargetChain)
// 2. Execute transfer (backend signs with Agent wallet)
exec, err := client.ExecuteIntent(ctx, resp.IntentID)
if err != nil {
log.Fatal(err)
}
log.Printf("Status: %s", exec.Status)
// 3. Query full receipt
intent, err := client.GetIntent(ctx, resp.IntentID)
if err != nil {
log.Fatal(err)
}
log.Printf("Final status: %s", intent.Status)
}
client, err := pay.NewClient("https://api-pay.agent.tech")
if err != nil {
log.Fatal(err)
}
resp, err := client.CreateIntent(ctx, &pay.CreateIntentRequest{
Email: "merchant@example.com",
Amount: "100.50",
PayerChain: "solana",
})
// Use resp.PaymentRequirements for payer to sign X402...
// After payment, submit the settle_proof:
proof, err := client.SubmitProof(ctx, resp.IntentID, settleProof)
Or run the bundled example:
git clone https://github.com/cross402/usdc-sdk-go
cd usdc-sdk-go
PAY_BASE_URL=https://api-pay.agent.tech \
PAY_API_KEY=your-api-key \
PAY_SECRET_KEY=your-secret-key \
go run ./example
Set PAY_INTENT_ID to skip creation and query an existing intent instead.
Authenticated (/v2) |
Public (/api) |
|
|---|---|---|
| Auth | WithBearerAuth |
None |
| Flow | CreateIntent → ExecuteIntent → GetIntent | CreateIntent → (payer pays) → SubmitProof → GetIntent |
| Use when | Integrator has no wallet; backend Agent signs | Integrator has payer's wallet; can sign X402 and submit settle_proof |
Both modes use the same Client — the prefix is selected automatically based on whether an auth option is provided.
Base64-encodes apiKey:secretKey and sends it as Authorization: Bearer <token>.
client, err := pay.NewClient(baseURL, pay.WithBearerAuth("api-key", "secret-key"))
The default HTTP client uses a 30-second timeout. Override with options:
client, err := pay.NewClient(baseURL,
pay.WithBearerAuth("id", "secret"),
pay.WithHTTPClient(&http.Client{
Timeout: 60 * time.Second,
Transport: customTransport,
}),
)
Or just change the timeout:
client, err := pay.NewClient(baseURL,
pay.WithBearerAuth("id", "secret"),
pay.WithTimeout(60 * time.Second),
)
| Method | Auth Required | Endpoint | Description |
|---|---|---|---|
GetSupportedChains |
No | GET /api/chains |
List payer and target chains the backend currently accepts |
CreateIntent |
No | POST {prefix}/intents |
Create a payment intent |
ExecuteIntent |
Yes | POST /v2/intents/{id}/execute |
Execute transfer with the Agent wallet (settles on the chosen target chain) |
SubmitProof |
No (public mode only) | POST /api/intents/{id} |
Submit settle_proof after payer completes X402 payment |
GetIntent |
No / Yes | GET {prefix}/intents?intent_id=... |
Get intent status and receipt. With auth, only intents owned by the calling agent are returned (404 otherwise — see below) |
ListIntents |
Yes | GET /v2/intents/list |
Paginated list of intents owned by the calling agent (most recent first) |
GetMe |
Yes | GET /v2/me |
Returns the calling agent's identity (id, number, name, status, wallets) |
chains, err := client.GetSupportedChains(ctx)
// chains.Chains — chains valid as PayerChain
// chains.TargetChains — chains valid as TargetChain
This endpoint is public-only on the backend; the SDK always hits /api/chains even when the client is configured with auth.
resp, err := client.CreateIntent(ctx, &pay.CreateIntentRequest{
Email: "merchant@example.com", // or Recipient (exactly one required)
Amount: "100.50", // 0.02–1,000,000 USDC, max 6 decimals
PayerChain: pay.ChainBase, // source chain
TargetChain: pay.ChainSolanaMainnet, // optional — omit to settle on "base"
})
CreateIntentRequest fields:
| Field | JSON | Required | Description |
|---|---|---|---|
Email |
email |
One of Email/Recipient | Recipient email address |
Recipient |
recipient |
One of Email/Recipient | Recipient wallet address |
Amount |
amount |
One of Amount/ToAmount | ExactOut target — the recipient receives exactly this (e.g. "100.50") |
ToAmount |
to_amount |
One of Amount/ToAmount | ExactIn amount — the payer sends exactly this (e.g. "100.50") |
PayerChain |
payer_chain |
Yes | Source chain (see Supported Chains) |
TargetChain |
target_chain |
No | Settlement chain. Omit to default to "base". Validate against GetSupportedChains().TargetChains. |
PayerAsset |
payer_asset |
No | Token the payer sends. Omit to default to usdc. See Assets. |
TargetAsset |
target_asset |
No | Token the recipient receives. Omit to default to usdc. See Assets. |
PayerAddress |
payer_address |
No | Optional payer wallet, screened advisorily against sanctions lists at create time. |
No request body — the backend uses the Agent wallet to sign the source-chain transfer and settle on the chosen target chain. Requires auth.
exec, err := client.ExecuteIntent(ctx, resp.IntentID)
// exec.Status is typically StatusTargetSettled
Submits a settle_proof after the payer completes X402 payment on the source chain. No auth required.
proof, err := client.SubmitProof(ctx, intentID, settleProof)
intent, err := client.GetIntent(ctx, intentID)
switch intent.Status {
case pay.StatusTargetSettled:
// use intent.TargetPayment for receipt; intent.TargetChain names the chain
case pay.StatusExpired, pay.StatusVerificationFailed:
// terminal failure
default:
// still processing — poll again
}
v2 ownership policy: when the client is configured with
WithBearerAuth,GetIntent(andExecuteIntent/SubmitProof) only return intents owned by the calling agent. Looking up an intent owned by a different agent — or one created via the public/apiflow — returns HTTP 404, not 403, so callers can't probe foreign intent IDs.
list, err := client.ListIntents(ctx, /*page*/ 1, /*pageSize*/ 20)
if err != nil {
log.Fatal(err)
}
for _, it := range list.Intents {
log.Printf("%s — %s (%s → %s)", it.IntentID, it.Status, it.PayerChain, it.TargetChain)
}
log.Printf("total=%d page=%d size=%d", list.Total, list.Page, list.PageSize)
page is 1-indexed; the server caps it at 1,000,000.pageSize must be in [1, 100]. Pass 0 for either parameter to use the server defaults (page 1, 20 per page).pageSize > 100, page < 0) are rejected by the SDK before reaching the API.me, err := client.GetMe(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("agent %s (%s) status=%s", me.AgentID, me.AgentNumber, me.Status)
log.Printf("base wallet=%s solana wallet=%s", me.WalletAddress, me.SolanaWalletAddress)
GetMe is served from middleware context — it does not hit the database — and is the cheapest way to verify an API key is live and bound to the agent the caller expects.
Intents expire 10 minutes after creation.
┌──────────────────┐
│ AWAITING_PAYMENT │
└────────┬─────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌─────────────────────┐
│ EXPIRED │ │ PENDING │ │ VERIFICATION_FAILED │
└──────────┘ └────┬─────┘ └─────────────────────┘
│
▼
┌────────────────┐
│ SOURCE_SETTLED │
└───────┬────────┘
│
▼
┌─────────────────┐
│ TARGET_SETTLING │
└────────┬────────┘
│
▼
┌────────────────┐
│ TARGET_SETTLED │
└────────────────┘
Use the status constants instead of bare strings:
| Constant | Value | Description |
|---|---|---|
pay.StatusAwaitingPayment |
AWAITING_PAYMENT |
Intent created, waiting for execution |
pay.StatusPending |
PENDING |
Execution initiated, processing |
pay.StatusVerificationFailed |
VERIFICATION_FAILED |
Source payment verification failed (terminal) |
pay.StatusSourceSettled |
SOURCE_SETTLED |
Source chain payment confirmed |
pay.StatusTargetSettling |
TARGET_SETTLING |
USDC transfer on the target chain in progress |
pay.StatusTargetSettled |
TARGET_SETTLED |
Transfer complete — check target_payment for receipt (terminal) |
pay.StatusPartialSettlement |
PARTIAL_SETTLEMENT |
Source settled but target failed — manual reconciliation required |
pay.StatusExpired |
EXPIRED |
Intent was not executed within 10 minutes (terminal) |
pay.StatusBlocked |
BLOCKED |
Compliance reject (OFAC SDN hit) — never retried (terminal) |
The set of payer and target chains is configured at runtime by the backend. Call GetSupportedChains to discover what's currently enabled — the two lists are independent (a chain can be a valid payer without being a valid settlement destination, and vice versa).
chains, err := client.GetSupportedChains(ctx)
// chains.Chains — valid as PayerChain
// chains.TargetChains — valid as TargetChain (omitted defaults to "base")
Use the Chain* constants instead of bare strings:
| Chain | Testnet Constant | Mainnet Constant |
|---|---|---|
| Solana | pay.ChainSolanaDevnet ("solana-devnet") |
pay.ChainSolanaMainnet ("solana-mainnet-beta") |
| Base | pay.ChainBaseSepolia ("base-sepolia") |
pay.ChainBase ("base") |
| BSC | pay.ChainBSCTestnet ("bsc-testnet") |
pay.ChainBSC ("bsc") |
| Polygon | pay.ChainPolygonAmoy ("polygon-amoy") |
pay.ChainPolygon ("polygon") |
| Arbitrum | pay.ChainArbitrumSepolia ("arbitrum-sepolia") |
pay.ChainArbitrum ("arbitrum") |
| Ethereum | pay.ChainEthereumSepolia ("ethereum-sepolia") |
pay.ChainEthereum ("ethereum") |
| Monad | pay.ChainMonadTestnet ("monad-testnet") |
pay.ChainMonad ("monad") |
| HyperEVM | pay.ChainHyperEVMTestnet ("hyperevm-testnet") |
pay.ChainHyperEVM ("hyperevm") |
| SKALE Base | — | pay.ChainSKALEBase ("skale-base", payer-only) |
| MegaETH | — | pay.ChainMegaETH ("megaeth", payer-only) |
ChainSKALEBase and ChainMegaETH are accepted only as PayerChain; the backend rejects them as TargetChain.
resp, err := client.CreateIntent(ctx, &pay.CreateIntentRequest{
Email: "merchant@example.com",
Amount: "100.50",
PayerChain: pay.ChainBase, // use constants instead of bare strings
TargetChain: pay.ChainSolanaMainnet, // optional — omit to settle on "base"
})
Each intent carries an asset on both legs — PayerAsset (what the payer sends on the source chain) and TargetAsset (what the recipient receives on the settlement chain). The two are independent of each other and of the chains, so you can mix them freely. Omit either to default it to usdc.
Use the Asset constants instead of bare strings:
| Constant | Value | Notes |
|---|---|---|
pay.AssetUSDC |
usdc |
Circle USDC. The default when an asset is omitted. |
pay.AssetUSDT0 |
usdt0 |
LayerZero USDT0 omnichain token (on Polygon, Tether's renamed legacy deployment). |
pay.AssetUSDT |
usdt |
Native Tether USDT. Some deployments are gated by the backend operator. |
resp, err := client.CreateIntent(ctx, &pay.CreateIntentRequest{
Email: "merchant@example.com",
Amount: "100.50",
PayerChain: pay.ChainArbitrum,
PayerAsset: pay.AssetUSDT0, // pay in USDT0 on Arbitrum
TargetChain: pay.ChainPolygon,
TargetAsset: pay.AssetUSDT0, // settle in USDT0 on Polygon
})
You can also cross assets — e.g. pay USDT0 on Arbitrum and settle USDC on Base — by setting TargetAsset: pay.AssetUSDC.
USDT0 is deployed on these mainnet chains at the time of writing:
| Chain | USDT0 |
|---|---|
Arbitrum (pay.ChainArbitrum) |
✓ |
Polygon (pay.ChainPolygon) |
✓ |
Monad (pay.ChainMonad) |
✓ |
HyperEVM (pay.ChainHyperEVM) |
✓ |
MegaETH (pay.ChainMegaETH) |
✓ (payer-only — rejected as TargetChain) |
There is no runtime discovery endpoint for per-chain assets, so an unsupported (chain, asset) pair is rejected by the backend with HTTP 400 (a *RequestError) — treat that as the source of truth if coverage changes.
PayerAsset and TargetAsset are echoed back on both CreateIntentResponse and GetIntentResponse.
The FeeBreakdown struct is returned in all intent response types (embedded via IntentBase):
| Field | JSON | Description |
|---|---|---|
SourceChain |
source_chain |
Source chain identifier |
SourceChainFee |
source_chain_fee |
Gas/network fee on the source chain |
TargetChain |
target_chain |
Settlement chain identifier |
TargetChainFee |
target_chain_fee |
Gas/network fee on the target chain |
PlatformFee |
platform_fee |
Platform service fee |
PlatformFeePercentage |
platform_fee_percentage |
Platform fee as a percentage |
TotalFee |
total_fee |
Sum of all fees |
Amount rules:
"0.000001", "123.45")The SDK uses typed errors and sentinel values for precise error matching.
RequestError — returned for HTTP 4xx/5xx responses from the API:
var reqErr *pay.RequestError
if errors.As(err, &reqErr) {
log.Printf("HTTP %d: %s", reqErr.StatusCode, reqErr.Body)
}
ValidationError — returned when the SDK rejects a request before it reaches the API (e.g. empty intent ID). Wraps a sentinel error for errors.Is matching:
var valErr *pay.ValidationError
if errors.As(err, &valErr) {
log.Printf("Invalid input: %s", valErr.Message)
}
UnexpectedError — wraps unexpected internal errors (JSON marshal failure, request creation, etc.):
var unexpErr *pay.UnexpectedError
if errors.As(err, &unexpErr) {
log.Printf("Unexpected: %v", unexpErr.Err)
}
Use errors.Is to check for specific validation failures:
if errors.Is(err, pay.ErrEmptyIntentID) {
// intent ID was empty
}
| Sentinel | Meaning |
|---|---|
ErrEmptyBaseURL |
baseURL was empty in NewClient |
ErrEmptyIntentID |
intentID was empty |
ErrEmptySettleProof |
settleProof was empty in SubmitProof |
ErrMissingAuth |
ExecuteIntent, ListIntents, or GetMe called without auth |
ErrNilParams |
CreateIntentRequest was nil |
ErrSubmitProofNotAllowed |
SubmitProof called with WithBearerAuth (use ExecuteIntent instead) |
ErrInvalidPagination |
page < 0, pageSize < 0, or pageSize > 100 in ListIntents |
| Status Code | Meaning |
|---|---|
| 400 | Bad request — invalid parameters, amount out of range, malformed input, or out-of-range pagination (page, page_size) |
| 401 | Unauthorized — missing or invalid credentials |
| 402 | Payment required — agent wallet has insufficient USDC to satisfy ExecuteIntent |
| 403 | Forbidden — reserved for future use; v2 ownership rejection returns 404, not 403 |
| 404 | Not found — intent does not exist or is owned by a different agent (uniform response prevents existence-leak probing) |
| 429 | Rate limited — too many requests (60 req/min/IP typical) |
| 503 | Service unavailable — temporary backend issue (e.g. proxy wallet temporarily out of funds) |
client, err := pay.NewClient(baseURL,
pay.WithBearerAuth(apiKey, secretKey),
pay.WithHTTPClient(&http.Client{
Timeout: 60 * time.Second,
Transport: &retryTransport{base: http.DefaultTransport},
}),
)
The API allows approximately 60 requests per IP per minute. On HTTP 429, implement exponential backoff:
var reqErr *pay.RequestError
if errors.As(err, &reqErr) && reqErr.StatusCode == 429 {
time.Sleep(backoff)
// retry
}
Your AI trading terminal assistant for US stocks, commodities, forex, and crypto.
Golang SDK for A2A Protocol
Publishes localhost services to the agentic web through self-hostable, trustless relays with x402 payments.
Building blocks for Agentic payments (x402, MPP, AP2) for TypeScript, Rust, Go, Python, Ruby, PHP, Lua, Kotlin and Swift.
Building blocks for Agentic payments (x402, MPP, AP2) for TypeScript, Rust, Go, Python, Ruby, PHP, Lua, Kotlin and Swift.
Go implementation of the x402 payment protocol