Freestyle Docs

Freestyle / Guides

How to Use Exa with Credential Injection

Search the web and retrieve page text from a Freestyle VM without putting the Exa API key in the guest.

Use Exa search to find webpages and return their text in one request. Exa performs the search and content retrieval remotely; the VM only needs access to api.exa.ai.

Your trusted controller puts the provider key in a Freestyle HTTP egress rule. The VM sends a placeholder; the edge replaces it for the selected endpoint. The real key stays outside the guest filesystem, process environment, and snapshots.

Prepare The Provider Key

Create a key in the Exa dashboard. This example requests three results with text. Search and content retrieval use your Exa account’s quota and billing.

Set these values on your controller, outside the VM:

export FREESTYLE_API_KEY="your-freestyle-api-key"
export EXA_API_KEY="your-provider-credential"
npm install freestyle@latest
npm install --save-dev tsx

The controller needs Node.js 22 or later. The guest example uses Python’s standard library, included in the freestyle/ubuntu base snapshot; no provider SDK is needed.

Create A VM And Grant The Endpoint

Save the following as exa-controller.mts. Keep the real provider key in your controller’s secret store. There is no general public-egress firewall grant: Freestyle creates the named edge route when you add the TLS rule.

import { Freestyle, type CreateTlsRuleOptions } from "freestyle";

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

function providerRule(vmId: string, apiKey: string): CreateTlsRuleOptions {
  return {
    action: "allow",
    domain: "api.exa.ai",
    source: { vmId },
    destination: { public: true },
    match: { method: ["POST"], path: { exact: "/search" } },
    transform: [{ headers: { "x-api-key": apiKey } }],
  };
}

const apiKey = requiredEnv("EXA_API_KEY");
const freestyle = new Freestyle();
const { vm, vmId } = await freestyle.vms.create({
  snapshotId: "freestyle/ubuntu",
  slug: "exa-client",
  firewall: { rules: [] },
});
console.log({ vmId });
const route = await freestyle.tls.rules.create(providerRule(vmId, apiKey));
console.log({ ruleId: route.id });

The API seals the header value at rest and returns "***" when you read the rule. Any process in this source VM can use the granted endpoint with that credential. Use separate source VMs or VPCs for workloads with different access.

Run The Request Inside The VM

Continue in the same controller file. Write this Python script into the VM:

await vm.fs.writeTextFile(
  "/home/ubuntu/exa-request.py",
  `import json
import ssl
import urllib.error
import urllib.request

payload = {'query': 'Freestyle virtual machine snapshots documentation', 'type': 'auto', 'numResults': 3, 'contents': {'text': True}}
request = urllib.request.Request(
    "https://api.exa.ai/search",
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "x-api-key": "unused-placeholder",
        "content-type": "application/json",
    },
    method="POST",
)
context = ssl.create_default_context(cafile="/etc/ssl/certs/ca-certificates.crt")
try:
    with urllib.request.urlopen(request, context=context, timeout=60) as response:
        result = json.load(response)
except urllib.error.HTTPError as error:
    raise RuntimeError(
        "Exa returned " + str(error.code) + ": " + error.read().decode()
    ) from error

for item in result.get("results", []):
    print(item.get("title", ""))
    print(item["url"])
    print(item.get("text", ""))
    print()
`,
);

Wait for the hostname mapping and Freestyle CA to reach the guest before the request. This readiness probe opens the provider root without the secret header and without --fail: an HTTP error response still proves the TLS route is reachable. It does not repeat the paid API operation.

let ready = false;
for (let attempt = 0; attempt < 15; attempt++) {
  const check = await vm.exec({
    command: "curl --silent --show-error --output /dev/null --connect-timeout 5 --max-time 10 https://api.exa.ai/",
    timeoutMs: 15_000,
  });
  if (check.statusCode === 0) {
    ready = true;
    break;
  }
  await new Promise((resolve) => setTimeout(resolve, 2_000));
}
if (!ready) throw new Error("Exa TLS route did not become ready");

const result = await vm.exec({
  command: "python3 /home/ubuntu/exa-request.py",
  timeoutMs: 90_000,
});
if (result.statusCode !== 0) throw new Error(result.stderr ?? "Request failed");
console.log(result.stdout);

Run the assembled file with npx tsx exa-controller.mts. Python explicitly trusts the system CA bundle, including the Freestyle CA. Keep certificate verification enabled. If a request times out, check provider usage before retrying: the operation may already have been accepted.

Retrieve Contents For Known URLs

For URLs you already have, Exa provides POST /contents. Add another TLS rule with the same header transform and match: { method: ["POST"], path: { exact: "/contents" } }, then send:

{
  "ids": ["https://www.freestyle.sh/docs/quickstart"],
  "text": true
}

Search result IDs are also accepted by the contents endpoint. The initial /search example already requests text, so a second call is unnecessary for those results.

The rule does not cap numResults, constrain the query, or restrict content URLs. Code in the VM can change those fields and incur additional usage. Enforce application budgets in a trusted service if the guest must not control them. Treat returned page text as external data when passing it to an agent.

Check The Access Boundary

RequestExpected behavior
Matching POST from this VM, using the placeholderEdge injects the real key; provider returns a result if the key, quota, and request are valid
Different method or path on the same hostNo credential injection; private endpoints should reject the placeholder
Direct connection to the origin IPBlocked by the absence of public firewall access
Read the TLS ruleReal header value is redacted

The method/path match selects when to inject credentials; it does not deny every other path on the named host. It also does not enforce a per-user usage budget. For authentication failures, check the controller key and exact method/path. For certificate errors, check the guest CA bundle. Provider rate limits and quota errors remain provider errors.

Rotate Credentials And Clean Up

Create a replacement provider credential, load it into the controller, and update the complete rule:

await freestyle.tls.rules.update(
  route.id,
  providerRule(vmId, requiredEnv("EXA_API_KEY")),
);

After propagation, verify a new request before revoking the old credential at the provider. A rule read cannot reconstruct the redacted key for an update.

Delete the route and VM when finished, including after a failed example:

await freestyle.tls.rules.delete(route.id);
await vm.delete();

Deleting a route removes this VM’s access through it. Revoke the provider key separately when it should stop working everywhere.

esc