Install
openclaw skills install @psyb0t/go-etoroUnofficial Go clients for the eToro public API, generated from the specs eToro publishes. The root package etoro (github.com/psyb0t/go-etoro) is an oapi-codegen REST client for https://public-api.etoro.com with a typed ...WithResponse method per endpoint (balances, market data, instruments, rates, candles, feeds, portfolios, transfers, posts). The etorows package (github.com/psyb0t/go-etoro/ws) is a typed WebSocket client for wss://ws.etoro.com/ws that authenticates, subscribes to topics and decodes instrument rate, candle and market status pushes and order and position updates through a Handler. Use when Go code needs to call the eToro API or stream eToro market data, or already imports go-etoro.
openclaw skills install @psyb0t/go-etoroUnofficial, typed Go clients for the eToro public API: REST in the root package etoro, WebSocket in etorows under ws/.
For install details, the request ID header, rate limit headers and the WebSocket push types, see references/setup.md.
x-api-key and x-user-key. Read them from environment variables (ETORO_API_KEY, ETORO_USER_KEY). Never hardcode them, commit them or log them, and never print a request with its headers.https://public-api.etoro.com and wss://ws.etoro.com/ws. Do not point the client at another host with the keys attached.PostMoneyTransfer...), posts, comments, likes and deletes. Call those only when the user asked for that exact action. Default to read-only endpoints.github.com/psyb0t/go-etoro or github.com/psyb0t/go-etoro/ws.client.gen.go and ws/etorows.gen.go are generated; see AGENTS.md in the repo.Every endpoint is a method on ClientWithResponses named after its operation ID plus WithResponse. Most take a *...Params struct, and most params structs carry a required XRequestId (a UUID per request).
import (
"context"
"fmt"
"net/http"
"os"
"github.com/google/uuid"
"github.com/psyb0t/go-etoro"
)
auth := etoro.WithRequestEditorFn(
func(_ context.Context, req *http.Request) error {
req.Header.Set("x-api-key", os.Getenv("ETORO_API_KEY"))
req.Header.Set("x-user-key", os.Getenv("ETORO_USER_KEY"))
return nil
},
)
client, err := etoro.NewClientWithResponses(
"https://public-api.etoro.com",
auth,
)
if err != nil {
return err
}
balances, err := client.GetBalancesWithResponse(
ctx,
&etoro.GetBalancesParams{XRequestId: uuid.New()},
)
if err != nil {
return err // transport failure
}
if balances.JSON200 == nil {
return fmt.Errorf("balances: %s", balances.Status())
}
total := balances.JSON200.TotalBalance // *float64
rates, err := client.GetMarketDataInstrumentsRatesWithResponse(
ctx,
&etoro.GetMarketDataInstrumentsRatesParams{
InstrumentIds: []int32{1001},
XRequestId: uuid.New(),
},
)
The error return covers only transport and decoding failures. An HTTP error status comes back as a response with JSON200 nil: check StatusCode() and Body, and on 429 read Headers429.RetryAfter.
import (
"crypto/rand"
"github.com/psyb0t/gen-asyncapi/asyncapirt/gorillaws"
"github.com/psyb0t/go-etoro/ws"
)
type handler struct {
etorows.UnimplementedHandler
}
func (handler) HandleInstrumentRateMessage(
ctx context.Context,
envelope etorows.InstrumentRateEnvelope,
) error {
for _, item := range envelope.Messages {
rate, err := item.ContentValue()
if err != nil {
return err
}
_ = rate.Ask // *string, rates arrive as strings such as "1.25"
}
return nil
}
conn, err := gorillaws.Dial(ctx, etorows.ServerEtoroURL)
if err != nil {
return err
}
client := etorows.NewClient(conn)
defer client.Close()
err = client.Authenticate(ctx, etorows.AuthenticateEnvelope{
ID: rand.Text(),
Operation: "Authenticate",
Data: etorows.AuthenticateData{
APIKey: os.Getenv("ETORO_API_KEY"),
UserKey: os.Getenv("ETORO_USER_KEY"),
},
})
if err != nil {
return err
}
err = client.Subscribe(ctx, etorows.SubscribeEnvelope{
ID: rand.Text(),
Operation: "Subscribe",
Data: etorows.SubscribeData{
Topics: []string{etorows.InstrumentRatesAddress("1001")},
},
})
if err != nil {
return err
}
return client.Run(ctx, handler{}) // blocks until ctx ends or the read fails
Embed etorows.UnimplementedHandler and override only the Handle... methods you need. A frame that matches no listed message goes to HandleUnknownMessage with an error that wraps asyncapirt.ErrUnknownMessage; the embedded default logs a warning and keeps reading, and returning an error there stops Run.