atxp

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 14 Imported by: 0

README

chit

Unofficial Go client and merchant library for ATXP. chit is not affiliated with, authorized by, or endorsed by Circuit & Chisel, the makers of ATXP. For the official, supported SDK, use their TypeScript one: https://github.com/atxp-dev/sdk

chit lets a Go program act as an ATXP client (pay for MCP tools), a merchant (charge callers for MCP tools), or both. It includes self-custodial x402 payments, so a payer needs no ATXP account at all.

The module path is a codename (chit), but the package is atxp, so it reads naturally:

Client
import (
    "github.com/justinstimatze/chit"
    "github.com/modelcontextprotocol/go-sdk/mcp"
)

c, _ := atxp.New(atxp.Config{ConnectionString: os.Getenv("ATXP_CONNECTION")})
sess, _ := c.Connect(ctx, "https://search.mcp.atxp.ai/")
defer sess.Close()
res, _ := sess.CallTool(ctx, &mcp.CallToolParams{
    Name:      "search_search",
    Arguments: map[string]any{"query": "..."},
})
Merchant

This example charges callers with self-custodial x402: the payer needs no ATXP account at all. It's the minimal shape. See examples/x402stranger for the complete version, including the X402PaymentRequirements cache a settle call needs (omitted here for brevity):

import (
    "encoding/json"
    "net/http"

    "github.com/justinstimatze/chit/server"
)

m, _ := server.New(server.Config{
    Destination:     server.StaticDestination{ID: "base:0xYourPayoutAddress"},
    ConnectionToken: connectionToken, // the merchant's own ATXP connection token
    PayeeName:       "my merchant",
})
price, _ := server.ParseAmount("0.01")

http.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) {
    pr := server.PaymentRequest{Price: price, User: "base:0xYourPayoutAddress", Resource: resourceURL}
    if detected := server.DetectProtocol(r.Header); detected != nil {
        pr.Session = m.OpenPaymentSession(*detected, server.SettlementContext{})
    }
    ch, err := m.RequirePayment(r.Context(), pr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    if ch != nil { // payment required: emit the challenge as a 402
        w.WriteHeader(http.StatusPaymentRequired)
        json.NewEncoder(w).Encode(ch.Data)
        return
    }
    if pr.Session != nil {
        m.CloseSession(r.Context(), pr.Session) // settles for real
    }
    w.Write([]byte("paid"))
})
Paying an OAuth-gated resource with no ATXP account

The merchant above accepts a bare 402; no OAuth needed. If the resource gates behind an OAuth 401 first, the payer needs some ATXP identity to complete the handshake, even though the payment itself stays self-custodial. atxp.HybridAccount splits the two: OAuth identity from a real ATXPAccount, payment signing from x402signer:

import (
    "github.com/justinstimatze/chit"
    "github.com/justinstimatze/chit/x402signer"
)

oauthAcct, _ := atxp.NewATXPAccount(os.Getenv("ATXP_CONNECTION"), nil)
signerAcct, _ := x402signer.NewFromPrivateKeyHex(os.Getenv("X402_PRIVATE_KEY"), "eip155:8453")

acct := &atxp.HybridAccount{Identity: oauthAcct, Payments: signerAcct}
c, _ := atxp.NewWithAccount(atxp.Config{}, acct)

The hosted-account client path does no on-chain crypto. Signing and settlement are delegated to ATXP over HTTP. A connection string is a wallet-grade secret: never log it, pass it as a CLI argument, or send it anywhere. x402signer/ is the exception. It signs EIP-3009 authorizations directly with a raw secp256k1 key, isolated to its own subpackage so the root package's dependency graph stays crypto-free.

Status

  • Client: done, validated end-to-end against production (discovery, dynamic client registration, OAuth, /sign, /authorize/auto, payment retry, a real paid tool call). Lives at the module root (package atxp).
  • Server / merchant: done. server.RequirePayment gates a metered call. CheckToken/CheckRequest authenticate callers. Verify/Settle finalize a push-payment retry credential. Merchant.OpenPaymentSession/ CloseSession let several calls sharing one retry credential settle once.
  • Self-custodial x402 signing (x402signer/): done, live-verified. It pays an x402 "exact"-scheme challenge by signing an EIP-3009 transferWithAuthorization with a raw key. No ATXP account, no OAuth, and no prior relationship with the merchant required. Real settlement is confirmed on Base mainnet and verified against the chain's own Transfer event log, not just an API response. atxp.HybridAccount pairs this with an ATXPAccount's OAuth identity for resources gated behind an OAuth 401 rather than a bare 402.

See docs/PROTOCOL.md's payment-modes table for exactly which combinations of payer identity, destination, and resource gate actually settle real money, with sequence diagrams for each.

Examples

  • examples/paidmcp: an OAuth-gated MCP server that charges $0.01 per tool call, plus a client that pays it. Demonstrates the hosted-account and hybrid x402 paths.
  • examples/x402stranger: a bare-402 merchant and a client with no ATXP account at all. Demonstrates stranger-to-stranger payment.

Testing

go build ./...
go test ./...                                        # unit, no network
go test -tags atxplive -run TestLive ./...            # client live; needs funded ATXP_CONNECTION
go test -tags serverlive -run TestLive ./server/...   # merchant live; needs funded ATXP_CONNECTION

The live tests need a funded ATXP account connection string, read from ATXP_CONNECTION or ~/.atxp/config. A freshly agent register-ed account is unfunded and fraud-blocked. Use a funded account's connection string from the dashboard Servers page instead. See docs/PROTOCOL.md for the full account-model details.

Docs

  • docs/PROTOCOL.md: the ATXP wire protocol as reverse-engineered from the TS SDK. Covers the OAuth and payment flow, the endpoint table, and every payment mode that's actually been tested live, with sequence diagrams.

License

MIT. This is a port of Circuit & Chisel's MIT-licensed TypeScript SDK; their copyright notice is retained in LICENSE. Contact: justin@justinstimatze.com

Documentation

Overview

Package atxp is a client for paid ATXP MCP tools (web search, image/video/music generation, etc.) using a hosted ATXP account (a connection string).

The hosted-account model means this client performs no on-chain crypto: every signing and settlement operation is an HTTP call to the ATXP accounts server. See docs/PROTOCOL.md for the full protocol, with file:line references into the reference TypeScript SDK (github.com/atxp-dev/sdk).

The shape mirrors the TS @atxp/client: an http.RoundTripper (transport.go) wraps an MCP Streamable HTTP transport and transparently handles the two legs of the protocol — OAuth authentication (oauth.go) and payment challenges (account.Authorize → the accounts server's /authorize/auto).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ATXPAccount

type ATXPAccount struct {
	// contains filtered or unexported fields
}

ATXPAccount is a hosted account identified by a connection string of the form

https://accounts.atxp.ai/?connection_token=<TOKEN>&account_id=<ID>

account_id is optional and resolved from /me on first use.

func NewATXPAccount

func NewATXPAccount(connectionString string, hc *http.Client) (*ATXPAccount, error)

NewATXPAccount parses a connection string into a hosted account. The http client is used for all accounts-server calls; pass nil for http.DefaultClient.

func (*ATXPAccount) AccountID

func (a *ATXPAccount) AccountID(ctx context.Context) (string, error)

func (*ATXPAccount) Authorize

func (*ATXPAccount) Origin

func (a *ATXPAccount) Origin() string

Origin is the accounts-server base URL (e.g. https://accounts.atxp.ai).

func (*ATXPAccount) SignChallenge

func (a *ATXPAccount) SignChallenge(ctx context.Context, codeChallenge string) (string, error)

func (*ATXPAccount) SpendPermission

func (a *ATXPAccount) SpendPermission(ctx context.Context, resourceURL string) (string, error)

type AccessToken

type AccessToken struct {
	ResourceURL  string
	AccessToken  string
	RefreshToken string
	ExpiresAt    int64 // unix seconds, 0 = unknown
}

AccessToken is a stored OAuth token for a (userID, resource) pair.

type Account

type Account interface {
	// AccountID returns the qualified account id ("atxp:<id>"), fetching it from
	// the accounts server's /me endpoint if it was not in the connection string.
	AccountID(ctx context.Context) (string, error)
	// SignChallenge asks the accounts server to mint the JWT that authorizes the
	// OAuth /authorize GET, binding the PKCE code_challenge to this account.
	SignChallenge(ctx context.Context, codeChallenge string) (jwt string, err error)
	// SpendPermission pre-authorizes spending for an MCP server during OAuth.
	// Returns "" (no error) if the account type does not support it.
	SpendPermission(ctx context.Context, resourceURL string) (token string, err error)
	// Authorize settles a payment challenge via /authorize/auto and returns the
	// protocol + opaque credential to attach to the retried request.
	Authorize(ctx context.Context, p AuthorizeParams) (AuthorizeResult, error)
}

Account is the credential + payment backend the transport delegates to. Only the hosted ATXPAccount is implemented; the interface keeps the transport decoupled from it (and leaves room for a future self-custodial account).

type AuthorizeParams

type AuthorizeParams struct {
	Protocols           []string        // "atxp", and optionally "x402"/"mpp"
	Amount              string          // decimal string, e.g. "0.01"
	Destination         string          // receiver address (maps to "receiver")
	Memo                string          // issuer / payee name
	PaymentRequirements json.RawMessage // x402 { x402Version, accepts } if present
	Challenges          json.RawMessage // mpp challenges array if present
}

AuthorizeParams is the payment-challenge data sent to /authorize/auto.

type AuthorizeResult

type AuthorizeResult struct {
	Protocol   string          `json:"protocol"` // "atxp" | "x402" | "mpp"
	Credential string          `json:"credential"`
	Context    json.RawMessage `json:"context,omitempty"`
}

AuthorizeResult is the credential returned by /authorize/auto.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client connects to ATXP MCP tool servers, transparently handling OAuth and per-call payments. account is the Account interface, not the concrete hosted ATXPAccount, so NewWithAccount can hand in a different backend (e.g. a self-custodial x402 signer) while reusing this same transport/OAuth glue.

func New

func New(cfg Config) (*Client, error)

New builds a Client backed by a hosted ATXPAccount from a connection string.

func NewWithAccount

func NewWithAccount(cfg Config, account Account) (*Client, error)

NewWithAccount builds a Client backed by any Account implementation (e.g. a self-custodial signer from a subpackage like x402signer) instead of the hosted ATXPAccount. cfg.ConnectionString is ignored; the other Config fields (HTTPClient, Store, CallbackURL) still apply.

func (*Client) Connect

func (c *Client) Connect(ctx context.Context, serverURL string) (*mcp.ClientSession, error)

Connect opens an MCP session to the given ATXP tool server (e.g. "https://search.mcp.atxp.ai/"). Payments and auth are handled transparently on each tool call. The caller owns the returned session and must Close it.

func (*Client) HTTPClient

func (c *Client) HTTPClient() *http.Client

HTTPClient returns an *http.Client whose transport performs the ATXP OAuth + payment handshake. It can be handed to any HTTP-based MCP transport, or used directly against ATXP REST endpoints.

type ClientCredentials

type ClientCredentials struct {
	ClientID     string
	ClientSecret string // empty for a public client
	RedirectURI  string
}

ClientCredentials are the dynamic-client-registration result for an authorization server, keyed by the server's issuer.

type Config

type Config struct {
	// ConnectionString is the hosted-account credential, e.g.
	// "https://accounts.atxp.ai/?connection_token=...&account_id=...".
	ConnectionString string
	// HTTPClient is used for accounts-server and OAuth calls. Optional.
	HTTPClient *http.Client
	// Store persists OAuth tokens/credentials. Defaults to an in-memory store.
	Store Store
	// CallbackURL is the OAuth redirect_uri registered with the auth server.
	// It is never actually navigated (ATXP returns the code directly), so the
	// default placeholder is fine.
	CallbackURL string
}

Config configures an ATXP client.

type HybridAccount

type HybridAccount struct {
	// Identity supplies AccountID, SignChallenge, and SpendPermission — the
	// OAuth-handshake half.
	Identity Account
	// Payments supplies Authorize — the actual payment-signing half.
	Payments Account
}

HybridAccount composes OAuth identity from one Account with payment authorization from another. It exists for the case where the account capable of completing the OAuth handshake (typically an ATXPAccount) is not the account that should actually pay: a self-custodial signer (e.g. x402signer.X402SignerAccount) has no ATXP identity of its own and cannot complete an OAuth handshake (see x402signer's package doc), but a resource gated behind an OAuth 401 still requires one before it ever issues a payment challenge.

An ATXPAccount for Identity plus an x402signer.X402SignerAccount for Payments is a live-verified combination: it settles a real x402 payment against an OAuth-gated third-party merchant, confirmed on-chain.

If the resource is gated purely by a bare 402 (no OAuth 401 first), Payments alone is sufficient and HybridAccount is unnecessary.

func (*HybridAccount) AccountID

func (h *HybridAccount) AccountID(ctx context.Context) (string, error)

func (*HybridAccount) Authorize

func (*HybridAccount) SignChallenge

func (h *HybridAccount) SignChallenge(ctx context.Context, codeChallenge string) (string, error)

func (*HybridAccount) SpendPermission

func (h *HybridAccount) SpendPermission(ctx context.Context, resourceURL string) (string, error)

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is a process-local Store.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty in-memory Store.

func (*MemoryStore) GetAccessToken

func (s *MemoryStore) GetAccessToken(userID, u string) (AccessToken, bool)

GetAccessToken returns the token for the exact path, falling back to parent paths up to the origin — mirroring oAuthResource.ts getAccessToken.

func (*MemoryStore) GetClientCredentials

func (s *MemoryStore) GetClientCredentials(issuer string) (ClientCredentials, bool)

func (*MemoryStore) GetPKCE

func (s *MemoryStore) GetPKCE(userID, state string) (PKCEValues, bool)

func (*MemoryStore) SaveAccessToken

func (s *MemoryStore) SaveAccessToken(userID, u string, t AccessToken)

func (*MemoryStore) SaveClientCredentials

func (s *MemoryStore) SaveClientCredentials(issuer string, c ClientCredentials)

func (*MemoryStore) SavePKCE

func (s *MemoryStore) SavePKCE(userID, state string, v PKCEValues)

type PKCEValues

type PKCEValues struct {
	URL           string // the resource URL the flow was started for (token key)
	CodeVerifier  string
	CodeChallenge string
	ResourceURL   string
}

PKCEValues are the per-authorization values stashed between building the authorization URL and handling the callback, keyed by OAuth state.

type RestrictionError

type RestrictionError struct {
	Op      string // the operation, e.g. "/sign"
	Code    string // restriction.error, e.g. "fraud_blocked"
	Message string // human-readable restriction.message
}

RestrictionError is returned when the accounts server rejects an operation because the account is restricted (e.g. a fresh, unverified account is "fraud_blocked" until a payment method is added). It is environmental, not a client bug: the request was well-formed and the server processed it.

func (*RestrictionError) Error

func (e *RestrictionError) Error() string

type Store

type Store interface {
	SavePKCE(userID, state string, v PKCEValues)
	GetPKCE(userID, state string) (PKCEValues, bool)

	SaveClientCredentials(issuer string, c ClientCredentials)
	GetClientCredentials(issuer string) (ClientCredentials, bool)

	SaveAccessToken(userID, url string, t AccessToken)
	GetAccessToken(userID, url string) (AccessToken, bool)
}

Store persists OAuth state. The in-memory implementation mirrors the TS MemoryOAuthDb closely enough for a single process; a deliberation server that wants tokens to survive restarts can supply a backing implementation.

Token lookup walks parent paths so a token issued for a server root also satisfies requests to sub-paths (see GetAccessToken).

Directories

Path Synopsis
examples
mcppay/client command
Command mcppay-client pays for an MCP tool call whose payment challenge arrives inline in the CallToolResult, rather than as an HTTP 402.
Command mcppay-client pays for an MCP tool call whose payment challenge arrives inline in the CallToolResult, rather than as an HTTP 402.
paidmcp command
Command paidmcp is a minimal, real MCP server that charges callers over ATXP for a tool call, using chit's server package.
Command paidmcp is a minimal, real MCP server that charges callers over ATXP for a tool call, using chit's server package.
paidmcp/client command
Command paidmcp-client drives the payer side of the examples/paidmcp demo: connects to a running paidmcp server over ATXP and calls its "ping" tool, paying for it via the full OAuth + payment retry flow.
Command paidmcp-client drives the payer side of the examples/paidmcp demo: connects to a running paidmcp server over ATXP and calls its "ping" tool, paying for it via the full OAuth + payment retry flow.
x402stranger command
Command x402stranger is a minimal x402 merchant that proves the actual promise of self-custodial x402 payments: a caller with no ATXP account, no OAuth relationship, and no prior interaction with this merchant can still pay for a resource and receive it.
Command x402stranger is a minimal x402 merchant that proves the actual promise of self-custodial x402 payments: a caller with no ATXP account, no OAuth relationship, and no prior interaction with this merchant can still pay for a resource and receive it.
x402stranger/client command
Command x402stranger-client pays for examples/x402stranger's resource with nothing but a raw private key: no ATXP account, no OAuth, no prior relationship with the merchant at all.
Command x402stranger-client pays for examples/x402stranger's resource with nothing but a raw private key: no ATXP account, no OAuth, no prior relationship with the merchant at all.
Package server is the merchant/server side of chit — the half that charges callers over ATXP.
Package server is the merchant/server side of chit — the half that charges callers over ATXP.
Package x402signer is a self-custodial x402 "exact"-scheme Account for chit's client.
Package x402signer is a self-custodial x402 "exact"-scheme Account for chit's client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL