Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ __pycache__/
*.pyc
*.pyo
*.pyd
.Python

# Go
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# Go workspace file
go.work
go.work.sum
# Build directories
/bin/
/dist/
# Test coverage
*.cover
*.coverprofile
coverage.html
coverage.out

# Go config files (contain sensitive information)
golang/examples/client/client_config.json
golang/examples/merchant/server_config.json

# Claude Code files
claude.md
93 changes: 93 additions & 0 deletions golang/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# x402 A2A Payment Protocol - Go Implementation

This is the Go implementation of the x402 payment protocol extension for A2A (Agent-to-Agent) communication.

## Features

- **x402 v2 Support**: This implementation uses x402 protocol version 2
- Payment verification and settlement
- Support for multiple blockchain networks (EVM and Solana)
- Merchant and client examples included

## Requirements

- Go 1.24.4 or later
- GEMINI_API_KEY environment variable (for image generation example)

## Quick Start

### Running the Merchant Server

1. Configure the server by creating `examples/merchant/server_config.json` based on `server_config.example.json`:

```json
{
"networkConfigs": [
{
"networkName": "eip155:84532",
"payToAddress": "0xYOUR_BASE_SEPOLIA_ADDRESS"
}
]
}
```

2. Set the GEMINI_API_KEY environment variable:
```bash
export GEMINI_API_KEY="your-api-key"
```

3. Run the server:
```bash
cd examples/merchant
go run . -port :8080 -facilitator https://www.x402.org/facilitator
```

### Running the Client

1. Configure the client by creating `examples/client/client_config.json` based on `client_config.example.json`:

```json
{
"networkKeyPairs": [
{
"networkName": "eip155:84532",
"privateKey": "0xYOUR_EVM_PRIVATE_KEY_HEX"
}
]
}
```

2. Run the client:
```bash
cd examples/client
go run . -merchant http://localhost:8080 -message "Generate an image of a sunset"
```

## Project Structure

- `core/`: Core implementation of x402 payment protocol
- `merchant/`: Merchant-side payment handling
- `client/`: Client-side payment handling
- `x402/`: x402 protocol state management
- `examples/`: Example implementations
- `merchant/`: Merchant server example with image generation service
- `client/`: Client example

## Configuration

### Server Configuration

The server requires a configuration file with network settings:
- `networkName`: Blockchain network identifier (e.g., "eip155:84532" for Base Sepolia)
- `payToAddress`: Address to receive payments

### Client Configuration

The client requires a configuration file with network key pairs:
- `networkName`: Blockchain network identifier
- `privateKey`: Private key for signing payment transactions

## x402 Protocol Version

This implementation uses **x402 protocol version 2** (`X402Version: 2`), which is set in the payment requirements when creating payment requests.

49 changes: 49 additions & 0 deletions golang/core/business/merchant_service_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package business defines the interfaces and types that merchants must implement
// to integrate their business services with the x402 payment framework.
// These interfaces allow developers to implement custom business logic while
// the framework handles payment verification and settlement.
package business

import (
"context"
)

type BusinessService interface {
Execute(ctx context.Context, prompt string) (string, error)

ServiceRequirements(prompt string) ServiceRequirements
}

type ServiceRequirements struct {
// Price is the payment amount required for the service (as a string, e.g., "1", "0.5")
Price string

// Resource is the resource identifier or URL associated with this service
Resource string

// Description is a human-readable description of the service
Description string

// MimeType is the MIME type of the resource (e.g., "application/json", "image/png")
MimeType string

// Scheme is the payment scheme (e.g., "exact", "at-least")
Scheme string

// MaxTimeoutSeconds is the maximum time in seconds before payment expires
MaxTimeoutSeconds int
}
118 changes: 118 additions & 0 deletions golang/core/client/a2a_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2aclient"
)

func NewA2AClient(ctx context.Context, merchantURL string) (*a2aclient.Client, error) {
agentCardURL := merchantURL + "/.well-known/agent-card.json"
agentCard, err := fetchAgentCard(ctx, agentCardURL)
if err != nil {
return nil, fmt.Errorf("failed to fetch AgentCard: %w", err)
}

extensionURIs := extractExtensionURIs(agentCard)
if len(extensionURIs) == 0 {
return nil, fmt.Errorf("no extensions found in AgentCard")
}

factory := a2aclient.NewFactory(
a2aclient.WithInterceptors(newExtensionHeaderInterceptor(extensionURIs)),
)

rpcEndpoint := determineRPCEndpoint(merchantURL, agentCard)
client, err := factory.CreateFromEndpoints(ctx, []a2a.AgentInterface{
{
URL: rpcEndpoint,
Transport: a2a.TransportProtocolJSONRPC,
},
})
if err != nil {
return nil, fmt.Errorf("failed to create A2A client from endpoints: %w. Ensure the server is running at %s", err, merchantURL)
}

return client, nil
}

func fetchAgentCard(ctx context.Context, url string) (*a2a.AgentCard, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

httpClient := &http.Client{Timeout: 10 * time.Second}
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch agent card: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

var card a2a.AgentCard
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
return nil, fmt.Errorf("failed to decode agent card: %w", err)
}

return &card, nil
}

func extractExtensionURIs(agentCard *a2a.AgentCard) []string {
extensions := agentCard.Capabilities.Extensions
uris := make([]string, 0, len(extensions))
for _, ext := range extensions {
if ext.URI != "" {
uris = append(uris, ext.URI)
}
}
return uris
}

func determineRPCEndpoint(merchantURL string, agentCard *a2a.AgentCard) string {
if agentCard.URL != "" && agentCard.PreferredTransport == a2a.TransportProtocolJSONRPC {
return agentCard.URL
}
return merchantURL + "/rpc"
}

func SendMessage(ctx context.Context, client *a2aclient.Client, message *a2a.Message) (*a2a.Task, *a2a.Message, error) {
messageParams := &a2a.MessageSendParams{
Message: message,
}
result, err := client.SendMessage(ctx, messageParams)
if err != nil {
return nil, nil, fmt.Errorf("failed to send message: %w", err)
}

switch v := result.(type) {
case *a2a.Task:
return v, nil, nil
case *a2a.Message:
return nil, v, nil
default:
return nil, nil, fmt.Errorf("received unexpected response type: %T", result)
}
}
26 changes: 26 additions & 0 deletions golang/core/client/a2a_extension_header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package client

import (
"context"

"github.com/a2aproject/a2a-go/a2aclient"
)

type extensionHeaderInterceptor struct {
a2aclient.PassthroughInterceptor
extensionURIs []string
}

func newExtensionHeaderInterceptor(extensionURIs []string) *extensionHeaderInterceptor {
return &extensionHeaderInterceptor{
extensionURIs: extensionURIs,
}
}

func (i *extensionHeaderInterceptor) Before(ctx context.Context, req *a2aclient.Request) (context.Context, error) {
if req.Meta == nil {
req.Meta = make(a2aclient.CallMeta)
}
req.Meta["X-A2A-Extensions"] = i.extensionURIs
return ctx, nil
}
44 changes: 44 additions & 0 deletions golang/core/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"context"
"fmt"

"github.com/a2aproject/a2a-go/a2aclient"
"github.com/google-agentic-commerce/a2a-x402/core/types"
)

type Client struct {
x402Client *X402Client
client *a2aclient.Client
}

func NewClient(merchantURL string, networkKeyPairs []types.NetworkKeyPair) (*Client, error) {
a2aClient, err := NewA2AClient(context.Background(), merchantURL)
if err != nil {
return nil, fmt.Errorf("failed to create A2A client: %w", err)
}
x402Client, err := NewX402Client(networkKeyPairs)
if err != nil {
return nil, fmt.Errorf("failed to create x402 client wrapper: %w", err)
}

return &Client{
x402Client: x402Client,
client: a2aClient,
}, nil
}
Loading
Loading